Assignment #5 Answers

Intermediate C Programming

UW Experimental College


Assignment #5 ANSWERS

Exercise 2. If you didn't use dynamically-allocated memory to hold long object and room descriptions, make that change now.

See the published answer to assignment 2, exercise 2.

Exercise 3. Rewrite newobject and newroom to dynamically allocate new structures, rather than parceling them out of the static objects and rooms arrays.

Rewriting newobject in object.c is easy enough:

struct object *
newobject(char *name)
{
struct object *objp;

objp = chkmalloc(sizeof(struct object));

strcpy(objp->name, name);
objp->lnext = NULL;
objp->attrs = NULL;
objp->contents = NULL;
objp->desc = NULL;

return objp;
}

However, it isn't quite so simple for newroom, because we need a way of looking through all the rooms we've got, for example in the findroom function. Therefore, we can't allocate room structures in isolation.

As suggested in the assignment, one possibility is to keep a linked list of all rooms allocated, by adding an extra ``next'' field to struct room in game.h:

struct room
	{
	char name[MAXNAME];
	struct object *contents;
	struct room *exits[NEXITS];
	char *desc;			/* long description */
	struct room *next;		/* list of all rooms */
	};
Now I can rewrite newroom in rooms.c:
static struct room *roomlist = NULL;

struct room *
newroom(char *name)
{
struct room *roomp;
int i;

roomp = chkmalloc(sizeof(struct room));

roomp->next = roomlist;		/* splice into list of all rooms */
roomlist = roomp;

strcpy(roomp->name, name);
roomp->contents = NULL;
for(i = 0; i < NEXITS; i++)
	roomp->exits[i] = NULL;
roomp->desc = NULL;

return roomp;
}
Splicing the new room into the list of all rooms is straightforward.

findroom must be rewritten slightly, to walk over the new room list rather than the old rooms array:

struct room *
findroom(char *name)
{
struct room *roomp;

for(roomp = roomlist; roomp != NULL; roomp = roomp->next)
	{
	if(strcmp(roomp->name, name) == 0)
		return roomp;
	}

return NULL;
}

We also have to decide what to do with the getentryroom function. (This is the function that gets called to decide which room the actor should initially be placed in.) It used to return, rather arbitrarily, the first room in the rooms array, which always happened to correspond to the first room in the data file, which was a reasonable choice. If we make the obvious change to getentryroom, and have it return the ``first'' room in the new room list:

struct room *
getentryroom(void)
{
return roomlist;	/* temporary */
}
it will not be so reasonable a choice, because our easy implementation of the list-splicing code in newroom above adds new rooms to the head of the list, such that the first room added, i.e. the first room in the data file, ends up at the end of the list. Depending on what your data file looks like, the game with the modifications shown so far will start out by dumping the player unceremoniously onto the back porch or into the basement (which might actually end up being an advantage for the player, if the basement tunnel is where the treasure is!).

If you wish, you can rewrite newroom to place new rooms at the end of the list, or getentryroom to return the tail of the list, to solve this problem. However, a better fix would be to allow the data file to explicitly specify what the entry room should be, rather than making the game code assume anything. We'll leave that for another exercise.

Exercise 4. Improve the code in io.c so that room exit lists can be placed directly in the room descriptions, rather than at the end.

The basic change is to the parsedatafile function. Whereas it used to parse lines at the end of the data file like

	roomexits kitchen s:hall e:stairway n:porch
we'll now make it parse lines like
	exit s hall
	exit e stairway
	exit n porch
which will occur within the description of the particular room (in this case, the kitchen) itself. (I've simplified things by putting three exits on three separate lines and eliminating the colon syntax; there was no particular reason for me to have done it in that more complicated way in the first place.)

Here is the new code for parsedatafile:

	else if(strcmp(av[0], "exit") == 0)
		{
		struct room *roomp;
		if(ac < 3)
			{
			fprintf(stderr, "missing exit or room name\n");
			continue;
			}
		if(currentroom == NULL)
			{
			fprintf(stderr, "exit not in room\n");
			continue;
			}
		roomp = findroom(av[2]);
		if(roomp != NULL)
			{
			/* already have room, so connect */
			connectexit(currentroom, av[1], roomp);
			}
		else	{
			/* haven't seen room yet; stash for later */
			stashexit(currentroom, av[1], av[2]);
			}
		}
This makes use of two auxiliary functions:
static void
connectexit(struct room *room, char *dirname, struct room *nextroom)
{
int dir;

if(strcmp(dirname, "n") == 0)
	dir = NORTH;
else if(strcmp(dirname, "e") == 0)
	dir = EAST;
else if(strcmp(dirname, "w") == 0)
	dir = WEST;
else if(strcmp(dirname, "s") == 0)
	dir = SOUTH;
else if(strcmp(dirname, "ne") == 0)
	dir = NORTHEAST;
else if(strcmp(dirname, "se") == 0)
	dir = SOUTHEAST;
else if(strcmp(dirname, "nw") == 0)
	dir = NORTHWEST;
else if(strcmp(dirname, "sw") == 0)
	dir = SOUTHWEST;
else if(strcmp(dirname, "u") == 0)
	dir = UP;
else if(strcmp(dirname, "d") == 0)
	dir = DOWN;
else	{
	fprintf(stderr, "no such direction \"%s\"\n", dirname);
	return;
	}

room->exits[dir] = nextroom;
}

struct stashedexit
	{
	struct room *room;
	char *dirname;
	char *nextroom;
	struct stashedexit *next;
	};

static struct stashedexit *stashedexits = NULL;

static void
stashexit(struct room *room, char *dirname, char *nextroom)
{
struct stashedexit *ep = chkmalloc(sizeof(struct stashedexit));
ep->room = room;
ep->dirname = chkstrdup(dirname);
ep->nextroom = chkstrdup(nextroom);
ep->next = stashedexits;
stashedexits = ep;
}
The stashexit function builds a linked list of stashed exits, allocating a new instance of the new struct stashedexit for each one, each containing the room pointer and the names of the exit direction and destination room. (The strings must also be copied to dynamically-allocated memory, because on entry to stashexit they are pointers back into the line or line2 array in parsedatafile, and those strings will be overwritten when we read the next line in the data file.)

Finally, we must arrange to resolve the stashed exits when we're done reading the data file and have had a chance to see the descriptions for all the rooms. Here is the change to the readdatafile function:

static void connectexit(struct room *, char *, struct room *);
static void stashexit(struct room *, char *, char *);
static void resolveexits(void);

readdatafile()
{
char *datfile = "dungeon.dat";
FILE *fp = fopen(datfile, "r");
if(fp == NULL)
	{
	fprintf(stderr, "can't open %s\n", datfile);
	return FALSE;
	}

parsedatafile(fp);
resolveexits();
fclose(fp);
return TRUE;
}

And here is the third auxiliary function:

static void
resolveexits()
{
struct stashedexit *ep, *nextep;

for(ep = stashedexits; ep != NULL; ep = nextep)
	{
	struct room *roomp = findroom(ep->nextroom);
	if(roomp == NULL)
		fprintf(stderr, "still no such room \"%s\"\n", ep->nextroom);
	else	connectexit(ep->room, ep->dirname, roomp);

	nextep = ep->next;
	free(ep->dirname);
	free(ep->nextroom);
	free(ep);
	}

stashedexits = NULL;
}
One aspect of this last function deserves mention. Why does it not use a more usual list-traversal loop, such as
	for(ep = stashedexits; ep != NULL; ep = ep->next)
What's with that temporary variable nextep? This loop is doing two things at once: traversing the linked list in order to resolve the stashed exits, and freeing the list as it goes. But when it frees the node (the struct stashedexit) it's working on, it also frees--and hence loses--the next pointer which that structure contains! Therefore, it makes a copy of the next pointer before it frees the structure.

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