Text adventure design in C

Started by
8 comments, last by Spinningcubes 8 years, 9 months ago

hello,

I am learning C and feel comfortable with most basics. I have created a very basic text adventure that worked ok but resulted in disgusting code.

Basically the game has a function for each room, with the same basic loop, printing of options, and a switch statement to get input (int input for predefined options in the menu) - and example of which is :


void beach(void)
{
	int here = 1;
	while (here)
	{
		fflush(stdin);
		system("cls");
		printf(TITLE);
		char *lookaround = "You are on a sandy beach, disoriented.";
		char *menu = "1. (Beach) Look Around\n\n2. Go North\n\n3. Go South\n\n4. Go East\n\n5. Go West\n\n6. Inventory\n\n\n> ";
		prints(menu);

		char choice;
		scanf("%c", &choice);
		switch (choice)
		{
			case '1': system("cls"); printf(TITLE); prints(lookaround); wait(); break;
			case '2': system("cls"); mountain(); break;
			case '3': system("cls"); market(); break;
			case '4': system("cls"); chapel(); break;
			case '5': system("cls"); swamp(); break;
			case '6': system("cls"); printf(TITLE); inventory(); break;
		}
	}
}

Basically each room function followed a similar theme, making it rather easy to add special options to certain rooms, but still poor design as to add on basically involved copying and pasting 1 room, then making alterations to the new room function. Before I go any further I am trying to improve my understanding and design, using structures and pointers. But whilst I understand how they work, I find it very difficult to figure out how best to implement it and would be very grateful for advice or links to help. Here is a tiny example of an attempt to better organise the program (though not doing anything of course)


#include <stdio.h>
#include <string.h>

void setupareas(void);

enum dir
{
	north = 1,
	east = 2,
	south = 3,
	west = 4
};

struct area
{
	char thedescription[200];
	char menu[200];
	char commands[3][100];
};

void printer(struct area *);

int main(void)
{
	setupareas();
	return 0;
}

void setupareas(void)
{
	struct area beach;
	strcpy(beach.thedescription, "A lovely beach");
	strcpy(beach.menu, "1. North\n\n2. East\n\n3. South\n\n4. West");
	printer(&beach);
}

void printer(struct area * p)
{
	printf("%s", p->thedescription);
	printf("\n\n");
	printf("%s", p->menu);
}	

But really I have no idea what I am doing. Any advice, examples on a basic text adventure setup for C or links to good sites would be great!

Please no C++ examples, recommendations that I should upgrade to C++ and use classes etc. I have my reasons for wanting to understand C and it would be great to get advice from a C expert.

Thanks!

Advertisement
The trick is to have a single game loop.
Inside you get input in a generic way, process it in a way which depends on the game's state, then output the current status to the screen.
This avoids repeating the loop, input and output code everywhere (possibly obfuscated by being slightly different each time).

The trick is to have a single game loop.
Inside you get input in a generic way, process it in a way which depends on the game's state, then output the current status to the screen.
This avoids repeating the loop, input and output code everywhere (possibly obfuscated by being slightly different each time).

.

To add to this - whenever you have to reuse the same code blocks many times, it would save a lot of typing ( and debugging ) if you spun them off into functions.

An example would be your "menu" - you could spin that off into a function that takes a location, and returns a number .

On a side note: You can 'cheat' in C by using/abusing structs as classes, but that is beyond the scope of this topic.

I cannot remember the books I've read any more than the meals I have eaten; even so, they have made me.

~ Ralph Waldo Emerson

You'll definitely want to create a stuct that can hold all the data about a specific area (name, description, neighboring areas, items that are there, commands that can be issued there, etc).

To begin with you can have a huge array of all these areas, as this is simple and will let you focus on learning about structs, and you can just have one function that allocates everything at the start of the game. Later on, you can learn how to dynamically allocate everything into a list.

You'll probably want to start with an array of strings (the C type of string) for commands, and one for items, and then your area struct can just store which indexes of these arrays are applicable.

By having all the areas stored in a struct array, you can now have just one game loop that only need to know the current area and which will work with all areas. And instead of having to have a menu for each area where he user types a number, they can type a command, and you can match that against the commands allowed for each area.

To help get you started, I've written a short example. It only has a few areas and a very small set of commands, but it should be enough for you to build on. As I mentioned earlier, they way this is allocating the data is not ideal (it will be fine for a very small game, but will quickly become cumbersome if you want to keep adding areas), but you can improve on that later.


#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct area {

    char name[20];
    char description[200];
    int neighbours[4];
    int num_items;
    int *items;
    int num_commands;
    int *commands;

} area;

typedef enum {NORTH, EAST, SOUTH, WEST} directions;

area locations[5];
char items[5][20];
char commands[3][20];  // special commands that can only be performed in some areas

void populate_items();
void populate_commands();
void populate_areas();

int main() {

    populate_items();
    populate_commands();
    populate_areas();

    int current_area = 0;
    int exit_status = 0;
    char command[20];

    while (exit_status == 0) {

        printf("\nYou are at %s\n", locations[current_area].name);
        scanf("%20s", command);
        if (strncmp(command, "quit", 20) == 0)
            exit_status = 1;
        else if (strncmp(command, "look", 20) == 0)
            printf("%s\n", locations[current_area].description);
        else if (strncmp(command, "search", 20) == 0) {
            if (locations[current_area].num_items > 0) {
                printf("You find:\n");
                for (int n = 0; n < locations[current_area].num_items; n++)
                    printf("%s\n", items[locations[current_area].items[n]]);
            }
            else
                printf("There is nothing here.\n");
        }
        else if (strncmp(command, "north", 20) == 0) {
            if (locations[current_area].neighbours[NORTH] != -1)
                current_area = locations[current_area].neighbours[NORTH];
            else
                printf("You can't go that way.\n");
        }
        else if (strncmp(command, "east", 20) == 0) {
            if (locations[current_area].neighbours[EAST] != -1)
                current_area = locations[current_area].neighbours[EAST];
            else
                printf("You can't go that way.\n");
        }
        else if (strncmp(command, "south", 20) == 0) {
            if (locations[current_area].neighbours[SOUTH] != -1)
                current_area = locations[current_area].neighbours[SOUTH];
            else
                printf("You can't go that way.\n");
        }
        else if (strncmp(command, "west", 20) == 0) {
            if (locations[current_area].neighbours[WEST] != -1)
                current_area = locations[current_area].neighbours[WEST];
            else
                printf("You can't go that way.\n");
        }


    }

    return 0;
}

void populate_items() {

    strncpy(items[0], "shell", sizeof items[0]);
    strncpy(items[1], "branch", sizeof items[1]);
    strncpy(items[2], "lighter", sizeof items[2]);
    strncpy(items[3], "water bottle", sizeof items[3]);
    strncpy(items[4], "torch", sizeof items[4]);

    return;

}

void populate_commands() {

    strncpy(commands[0], "burn", sizeof commands[0]);
    strncpy(commands[1], "move", sizeof commands[1]);
    strncpy(commands[2], "break", sizeof commands[2]);

    return;

}

void populate_areas() {

    strncpy(locations[0].name, "the beach", sizeof locations[0].name);
    strncpy(locations[0].description, "A sandy beach covered in shells.", sizeof locations[0].description);
    memcpy(locations[0].neighbours, (int[]) { -1, 1, -1, -1 }, sizeof locations[0].neighbours);
    locations[0].num_items = 1;
    locations[0].items = malloc(locations[0].num_items * sizeof(int));
    locations[0].items[0] = 0;
    locations[0].num_commands = 0;
    locations[0].commands = NULL;

    strncpy(locations[1].name, "the forest", sizeof locations[0].name);
    strncpy(locations[1].description, "A dark and spooky forest.", sizeof locations[0].description);
    memcpy(locations[1].neighbours, (int[]) { 2, -1, 4, 0 }, sizeof locations[0].neighbours);
    locations[1].num_items = 1;
    locations[1].items = malloc(locations[0].num_items * sizeof(int));
    locations[1].items[0] = 1;
    locations[1].num_commands = 0;
    locations[1].commands = NULL;

    strncpy(locations[2].name, "the hill", sizeof locations[0].name);
    strncpy(locations[2].description, "A windswept hilltop covered in tussock.", sizeof locations[0].description);
    memcpy(locations[2].neighbours, (int[]) { -1, 3, 1, -1 }, sizeof locations[0].neighbours);
    locations[2].num_items = 2;
    locations[2].items = malloc(locations[0].num_items * sizeof(int));
    locations[2].items[0] = 2;
    locations[2].items[1] = 4;
    locations[2].num_commands = 0;
    locations[2].commands = NULL;

    strncpy(locations[3].name, "the mountain", sizeof locations[0].name);
    strncpy(locations[3].description, "A tall lonely mountain.", sizeof locations[0].description);
    memcpy(locations[3].neighbours, (int[]) { -1, -1, -1, 2 }, sizeof locations[0].neighbours);
    locations[3].num_items = 0;
    locations[3].items = NULL;
    locations[3].num_commands = 0;
    locations[3].commands = NULL;

    strncpy(locations[4].name, "the river", sizeof locations[0].name);
    strncpy(locations[4].description, "A mighty river, too fierce to cross.", sizeof locations[0].description);
    memcpy(locations[4].neighbours, (int[]) { 1, -1, -1, -1 }, sizeof locations[0].neighbours);
    locations[4].num_items = 1;
    locations[4].items = malloc(locations[0].num_items * sizeof(int));
    locations[4].items[0] = 3;
    locations[4].num_commands = 0;
    locations[4].commands = NULL;

    return;
}

wow thanks Lenny, that's awesome. I will study this code and try to build ontop of it.

Holy shit.

There's so much wrong in this thread I can hardly believe.

Do not hardcode locations/rooms in code.

Do not produce structs by using C code.

Lenny, seriously, I sympathize with the structure you proposed, but you should really have spared us from demonstrating the terrible practice of mixing code with assets. Have phun managing the numeric index for 'touch' information.

True solution: data driven architecture. Here's a JSON to populate areas (not the same thing, as I couldn't be bothered in keeping track of the indices), then you only have to parse it and get your structures out.


{
"locations": {
"beach" : {
"desc": "A sandy beach covered in shells.",
"near": [ [ "north", "forest" ] ]
},
"forest" : {
"desc": "A dark and spooky forest.",
"near": [ [ "south", "beach" ], [ "east", "hill" ] ],
"items": [ "temple_key" ]
},
"hill" : {
"desc": "A windswept hilltop covered in tussock.",
"near": [ [ "west", "forest" ], [ "north", "mountain" ] ],
"items": [ "tin_can", "rotting_adventurer_corpse" ]
},
"mountain" : {
"desc": "A tall lonely mountain.",
"near": [ [ "south", "hill" ] ]
}
}
}

Bonus: here's how it looks in notepad++ json viewer.

Left as an exercise: HTML5+CSS landscape editor!

Previously "Krohm"

I'd like to reinforce Krohm's statement: Do not hardcode locations/rooms in code.
I think a practical approach that is worth learning (and i remember it being used in quite a few MUDs) would be linked-lists of structs.

Each room-struct would have a next-pointer to another room.

Store your rooms in text-files, JSON is nice, but just some plaintext might well be enough.

You parse your files and create rooms linking each one to the one before. Last one gets next = NULL, so you know that's where your list ends.

Of course you keep a room* to the first room to access the list.

In the datafiles you specify the rooms that can be accessed from a room, by some unique identifier, a number preferably.

After parsing rooms you setup each room-struct's pointers to accessible rooms, so you don't have to search the list later on.

When player types a "go north" or similar command, you know what room to use.

All strings unique to a room are in that room's text-data, and have been read to the room's struct by the game, like name, description, looktext and so on, or even items to be picked up in that room.

The player actions/commands you could put in an array of command-structs. Each command-struct would hold a string and a function-pointer to a function to be called when player enters that string. For more learning you create a hash-map of the commands so you can look up functions corresponding to command in style!

You might want to look into source of classic text-games, like Rivers of Mud, or take a look around here.


Lenny, seriously, I sympathize with the structure you proposed, but you should really have spared us from demonstrating the terrible practice of mixing code with assets. Have phun managing the numeric index for 'touch' information.

I agree that it's a bad practice, and I'd never do it myself, but trying to get someone who is still trying to get their head around using structs to learn JSON and file IO on top of everything else is not a good idea either. Try reading a textbook designed for beginners and see how many topics are covered before you get on to that.

Plus, if I'd added that extra functionality to the code, the OP would have nothing to improve on, which is an important step in learning.


I think a practical approach that is worth learning (and i remember it being used in quite a few MUDs) would be linked-lists of structs.

I agree, which is why I suggested he do that after he's figured out the basics.

Oh well, if its constant data it could be statically initialized to cut down on needless initialization code.
Using a 2D array for the data and looking at adjacent fields might be easier to create and use than structs with a couple of pointers.
For such a beginner project recompiling when changing some data would not be that bad, as it should go pretty fast. Later on he might want to learn loading from a file, but something simple like csv format would be easier to use (telling to use JSON/JavaScript/HTLM/CSS is kind of missing the point when someone is trying to learn C).

I suggest you put the areas in a text file and load them into your program as your next step. It is not that hard and you can use a simple format where each line is a single command and a parameter.


AREA SandyBeach
LOOK You are on a sandy beach, disoriented.
GONORTH Mountain
GOSOUTH Market
GOEAST Chapel
GOWEST Swamp

AREA Swamp
LOOK The swamp is filled with frogs.
GOEAST SandyBeach

Use fopen() to open the file and fclose() to close it when you are done. In a loop use getline() to read each new line into a temporary buffer. Using strstr() you can compare the temporary buffer with the commands you support and handle each one. If the command is AREA create a new area and all the following commands act on the last created area . You can get the rest of the data after the command by indexing the temporary buffer with the length of the command word.

So your main loop will have the index of the current area (if you keep them all in an array). When the player say go east you look up the goeast string in your current area. If it is set to anything search your area array for a area with that name. If found change the current area index.

When you have it so you can walk around forward and back between areas i would suggest the next step is to add flags to the room. If you walk into the area with the flag DARK for example you need to have a light in your inventory to even see something. Add a flags string to your area struct and add a new command to set it. Then the code that handle the display of the area text can check for the DARK flag and handle it.


FLAG DARK|COLD|HASCAKE

@spinningcubes | Blog: Spinningcubes.com | Gamedev notes: GameDev Pensieve | Spinningcubes on Youtube

This topic is closed to new replies.

Advertisement