Assignment #7

Intermediate C Programming

UW Experimental College


Assignment #7

Handouts:

Assignment #7
Assignment #6 Answers
Class Notes, Chapter 25

Exercises:

  1. Adding new command verbs to the game quickly becomes cumbersome. As long as they all go into commands.c, they can potentially be invoked on any object and with any tool. So if a new command is (or ought to be) specific to some tool or direct object, the code to implement it has to spend most of its time verifying that the overcreative player isn't trying to apply the new command verb in some wildly imaginative but utterly inappropriate way (``sharpen gum with feather''; ``pierce idea with boot''; etc.). It would be far better, in several respects, if scraps of code, for implementing certain command verbs, could be attached directly to objects, so that they would only be invoked if those objects were involved (and, additionally, so that the same verb might possibly perform different actions if applied to different objects). The notion of attaching code to data structures is one of the fundamentals of Object Oriented Programming, and it's a curious coincidence (or is it?) that the data structures which in this program we'd like to attach code to are in fact what we call ``objects.''

    We're going to add a new field, of type pointer-to-function, to our struct object. This field will contain a pointer to a per-object function which handles one or more command verbs unique to that object. (If an object has no unique verbs it wishes to process, its function pointer will be null.) Here is the new field for struct object in game.h:
    	int (*func)(struct actor *, struct object *, struct sentence *);
    
    Notice that func takes the same arguments as all the other command functions which we split commands.c up into last week.

    If an object contains some custom verb-handling code, it will be called from docommand in commands.c. Before trying all of the default commands, it first sees if either of the two objects which a command might reference (the direct object, or the object of the preposition) has its own function. If so, it calls that (those) function(s).

    Now, those functions might or might not know how to handle the particular verb the user typed. If not, control should flow through to the default, global command functions. (For example, you can still take or drop or examine just about any object, even if it has its own special code for when you try to do other things with it, and the object's function shouldn't have to--and couldn't--repeat all the other code for all the other verbs.) To coordinate things, then, each function that tries to implement one of the user's commands can return various status codes.

    There are four status codes (which go in game.h):
    	#define FAILURE		0	/* command completed unsuccessfully */
    	#define SUCCESS		1	/* command completed successfully */
    	#define CONTINUE	2	/* command not completed */
    	#define ERROR		3	/* internal error */
    
    FAILURE means that the command completed, but unsuccessfully (the player couldn't do what he tried to do). SUCCESS means that the command completed, successfully. CONTINUE means that the function which was just called did not implement the command, and that the game system (i.e. the docommand function) should keep trying, by calling other functions (if any). Finally, ERROR indicates an internal error of some kind.

    Here is the new code for docommand. This goes at the top of the function, before it calls findcmd:
    if(cmd->object != NULL && cmd->object->func != NULL)
    	{
    	r = (*cmd->object->func)(player, cmd->object, cmd);
    	if(r != CONTINUE)
    		return r;
    	}
    	
    if(cmd->xobject != NULL && cmd->xobject->func != NULL)
    	{
    	r = (*cmd->xobject->func)(player, cmd->xobject, cmd);
    	if(r != CONTINUE)
    		return r;
    	}
    


    You'll also need to add the line
    	objp->func = NULL;
    
    to the newobject function in object.c.

    Next we'll need some actual functions (special-purpose ones) for various objects to use. Here are a few examples:
    int hammerfunc(struct actor *player, struct object *objp,
    					struct sentence *cmd)
    {
    	if(strcmp(cmd->verb, "break") != 0)
    		return CONTINUE;
    	if(objp != cmd->xobject)
    		return CONTINUE;
    
    	if(cmd->object == NULL)
    		{
    		printf("You must tell me what to break.\n");
    		return FAILURE;
    		}
    	if(!contains(player->contents, cmd->xobject))
    		{
    		printf("You have no %s.\n", cmd->xobject->name);
    		return FAILURE;
    		}
    
    	setattr(cmd->object, "broken");
    	printf("Oh, dear.  Now the %s is broken.\n", cmd->object->name);
    
    	return SUCCESS;
    }
    
    int toolfunc(struct actor *player, struct object *objp,
    					struct sentence *cmd)
    {
    	if(strcmp(cmd->verb, "fix") != 0)
    		return CONTINUE;
    	if(objp != cmd->xobject)
    		return CONTINUE;
    
    	if(cmd->object == NULL)
    		{
    		printf("You must tell me what to fix.\n");
    		return FAILURE;
    		}
    	if(!hasattr(cmd->object, "broken"))
    		{
    		printf("The %s is not broken.\n", cmd->object->name);
    		return FAILURE;
    		}
    	if(!contains(player->contents, cmd->xobject))
    		{
    		printf("You have no %s.\n", cmd->xobject->name);
    		return FAILURE;
    		}
    
    	unsetattr(cmd->object, "broken");
    	printf("Somehow you manage to fix the %s.\n", cmd->object->name);
    
    	return SUCCESS;
    }
    
    int cutfunc(struct actor *player, struct object *objp,
    					struct sentence *cmd)
    {
    	if(strcmp(cmd->verb, "cut") != 0)
    		return CONTINUE;
    	if(objp != cmd->xobject)
    		return CONTINUE;
    
    	if(cmd->object == NULL)
    		{
    		printf("You must tell me what to cut.\n");
    		return FAILURE;
    		}
    	if(!contains(player->contents, cmd->xobject))
    		{
    		printf("You have no %s.\n", cmd->xobject->name);
    		return FAILURE;
    		}
    	if(!hasattr(cmd->object, "soft"))
    		{
    		printf("I don't think you can cut the %s with the %s.\n",
    					cmd->object->name, cmd->xobject->name);
    		return FAILURE;
    		}
    
    	printf("The %s is now cut in two.\n", cmd->object->name);
    
    	return SUCCESS;
    }
    
    int playfunc(struct actor *player, struct object *objp,
    					struct sentence *cmd)
    {
    	if(strcmp(cmd->verb, "play") != 0)
    		return CONTINUE;
    	if(objp != cmd->object)
    		return CONTINUE;
    
    	if(!hasattr(cmd->object, "immobile"))
    		{
    		if(!contains(player->contents, cmd->object))
    			{
    			printf("You don't have the %s.\n", cmd->object->name);
    			return FAILURE;
    			}
    		}
    
    	printf("You're not quite ready for Carnegie hall,\n");
    	printf("but you do manage to force out a recognizable tune.\n");
    
    	return SUCCESS;
    }
    
    I put these functions in a new source file, objfuncs.c. (It is not on the disk.)

    The first three of these are basically simplifications of the breakcmd, fixcmd, and cutcmd functions (or, at any rate, the code for ``break'', ``fix'', and ``cut'') from commands.c. Since these functions will be attached to certain objects, and called only when those objects are involved in the command, these functions do not have to do quite so much checking. For example, hammerfunc does not have to make sure that the implement being used is heavy, because hammerfunc will only be attached to the hammer (or perhaps to other objects which it might be appropriate to smash things with).

    However, there are two new things these functions do need to check. Since they will be called first, before the default functions in commands.c, they must defer to those default functions for most verbs. They must check to see that they're being called for a verb they recognize, and if not, return the status code CONTINUE indicating that some other code down the line should keep trying. Also, these functions can be called when the object they're attached to is either the direct object of the sentence or the object of a preposition. When one of these functions is called for an object which appears as the direct object, the objp parameter is a copy of cmd->object, as before. When they're called for objects which are objects of prepositions, however, objp is a copy of that object pointer (see the new code for docommand above). So these commands must typically check that they're being used in the way they expect--for example, if hammerfunc didn't check to make sure that it was being used as a prepositional object, it might incorrectly attempt to handle a nonsense sentence like
    	break hammer
    


    The next step is to hook these functions up to the relevant objects. To do that, we'll need to invent a new object descriptor line for the data file, and add some new code to readdatafile in io.c to parse it. We'll rig it up so that we can say
    	object hammer
    	attribute heavy
    	func hmmerfunc
    
    (We'll keep the attribute ``heavy'' on the hammer, because it might be useful elsewhere, although we won't be needing it for the old breakcmd function any more.) The new code for io.c is pretty simple, but before we can write it, we have to remember that there's a significant distinction between the name by which we know a function and the internal address, or function pointer, by which the compiler knows it. When we write
    	func hammerfunc
    
    in the data file, it's obvious to us that we want the hammer object's func pointer to be hooked up to hammerfunc, but it is not at all obvious to the compiler. In particular, the compiler is not going to be looking at our data file at all! Code in io.c is going to be looking at the data file, and it's going to be doing it as the program is running, well after the compiler has finished its work. So we're going to have to ``play compiler'' just a bit, to match up the name of a function with the function itself.

    This will actually be rather simple to do, because matching up a name (i.e. a string) with a function is exactly what the struct cmdtab structure and findcmd function do. So if we build a second array of cmdtab structures, holding the names and function pointers for functions which it's appropriate to attach to objects, all we have to add to readdatafile is this scrap of code:
    	else if(strcmp(av[0], "func") == 0)
    		{
    		struct cmdtab *cmdtp = findcmd(av[1], objfuncs, nobjfuncs);
    		if(cmdtp != NULL)
    			currentobject->func = cmdtp->func;
    		else	fprintf(stderr, "unknown object func %s\n", av[1]);
    		}
    
    The new array of cmdtab structures is called objfuncs. I chose to define it at the end of the new source file objfuncs.c:
    struct cmdtab objfuncs[] =
    	{
    	"hammerfunc",	hammerfunc,
    	"toolfunc",	toolfunc,
    	"cutfunc",	cutfunc,
    	"playfunc",	playfunc,
    	};
    
    #define Sizeofarray(a) (sizeof(a) / sizeof(a[0]))
    
    int nobjfuncs = Sizeofarray(objfuncs);
    
    So that readdatafile can access the objfuncs array, we need these two lines at the top of io.c:
    	extern struct cmdtab objfuncs[];
    	extern int nobjfuncs;
    
    An explanation of the nobjfuncs variable is in order. When we called findcmd in commands.c to search our main list of commends, the call looked like
    	cmdtp = findcmd(cmd->verb, commands, Sizeofarray(commands));
    
    But our little Sizeofarray() macro uses sizeof, and sizeof works (rather obviously) only for objects which the compiler knows the size of. In io.c, where we have
    	extern struct cmdtab objfuncs[];
    
    the code sizeof(objfuncs) would not work, because in that source file, all the compiler knows about objfuncs is that it is an array which is defined (and which therefore has its size set) somewhere else. So we need to do the computation of the number of elements in the array within the source file objfuncs.c where the array is defined, and store this number in a second global variable, nobjfuncs, so that code in io.c can get its hands on it.

    Having made these changes, you should be able to add the line
    	func hammerfunc
    
    to the description of the hammer in dungeon.dat, and the line
    	func toolfunc
    
    to the description of the pliers or some other tool-like object, and the line
    	func cutfunc
    
    to the description of the knife or some other sharp object. You can also add the lines
    	object violin
    	func playfunc
    	object end
    
    	object piano
    	attribute immobile
    	func playfunc
    	object end
    
    to try out the sample ``play'' function. Notice that playfunc makes you pick up the violin before playing it, but since the piano is immobile, you can just walk up to it and start playing.
  2. Rewrite the lockcmd and unlockcmd functions (from Assignment 4) as object functions.
  3. Invent some more object functions and some objects for them to be attached to. Examples: weapons, other tools, magic wands...
  4. Since object functions (if present) are called before the default functions in commands.c, they can not only supplement the commands in commands.c, they can also override (on a per-object basis) the default behavior of an existing verb. Invent a shower object. (If your dungeon doesn't already have a bathroom, perhaps you'll need to add that, too.) Rig it up so that if the player types
    	take shower
    
    the game will print neither ``Taken'' nor ``The shower cannot be picked up'' but rather something cute like ``After spending 20 luxurious minutes in the shower, singing three full-length show tunes, and using up all the hot water, you emerge clean and refreshed.''
  5. Since per-object functions are called before default functions, and since they can defer to those default functions by returning CONTINUE, another kind of customization is possible. A per-object function can recognize a verb, do some processing based upon it, but then return CONTINUE, so that the default machinery will also act. Implement a magic sword object so that when the player picks it up, the messages ``As you pick it up, the sword briefly glows blue'' and ``Taken'' are printed. The first message is printed by the sword's custom function, and the second one by the normal ``take'' machinery in commands.c.
  6. Implement a container of radioactive waste. Arrange that if the player tries to pick it up, the message ``the container is far too dangerous to pick up'' is printed, unless the player is wearing a Hazmat suit, in which case the container can be picked up normally. Also implement the Hazmat suit object. (You can either just have the player pick up the Hazmat suit, or for extra credit, arrange that the Hazmat suit implement a custom ``wear'' verb.)


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