Contents
21. Multi-player
|
How to program a text adventure in C
by Ruud Helderman
<r.helderman@hccnet.nl>
Licensed under
MIT License
21. Multi-player
It wasn’t long after the first text adventures appeared,
that somebody came with the idea to make it a multi-user event.
MUDs
flourished on university networks,
then gradually found their way to home users through
BBSs.
Later, widespread internet access paved the way for
MMOs,
but by that time, attention had already shifted
from text-based gaming to 3D graphics.
Sure, the whole world is
texting,
but even among gamers, few are aware you can use that for
role-playing.
Multi-player is a rather broad term. It could stand for:
- Having multiple
player characters
in the same game.
- Having multiple
users
together playing within the same game session.
In most cases, both apply, and there will be a one-on-one relationship,
i.e. every user controls one player character.
But it doesn’t have to be this way:
it is very well possible to make a game
where a single user is controlling multiple player characters.
That may not be everybody’s idea of fun, but technically,
these are separate dimensions, largely independent of each other.
And since there’s plenty of work to be done,
it makes sense to divide it across two chapters.
Below, we will introduce the concept of having more than one player character,
while at the same time keeping the game a single-user experience.
In the next chapter, we will make the game truly multi-user.
In its simplest form, multiple player characters means
the user will be given the opportunity to switch between characters,
controlling them one at a time.
Imagine we have two player characters: Jack and Jill.
We could introduce a command ‘play’
that allows the user to take control of either one of those characters.
The following four commands would then make
Jack pick up the club, and Jill pick up the dagger.
play jack
get club
play jill
get dagger
|
Multiple player characters could be a nice opportunity
to have puzzles in your game that can only be solved
by having the characters cooperate in a certain way.
For example, one character could be used to distract or lure away the guard,
while the other character sneaks into the cave.
Puzzles might also demand the user to carefully think about
each character’s strengths and weaknesses.
It won’t be until the next chapter
that we will benefit from the social aspect of multi-player:
being able to really interact (remotely) with other human beings.
But since we are expanding our vocabulary anyway (the verb play),
we might as well take the opportunity
to introduce some ‘social’ commands.
- say -
to send a message to everybody present in the same location.
- whisper -
to send a message to a specific player character,
without exposing it to others present in the same location.
- emote -
similar to say, but for
non-verbal communication.
MUD-like games typically use this to facilitate
role-playing.
Let’s start by adding the new verbs to our vocabulary.
Sample output |
Welcome to Little Cave Adventure.
You are in the waiting room.
You see:
walls all around you
--> play jack
You are Jack. Jack is a fearsome warrior.
--> look around
You are in an open field.
You see:
a silver coin
a burly guard
Jill
a cave entrance to the east
dense forest all around
a lamp
a wooden club
a dagger
--> get club
You pick up a wooden club.
--> play jill
You are Jill. Jill is a vicious valkyrie.
--> look around
You are in an open field.
You see:
a silver coin
a burly guard
Jack
a cave entrance to the east
dense forest all around
a lamp
a dagger
--> say Where am I?
You say: Where am I?
--> whisper to jack I need a weapon.
You whisper to Jack: I need a weapon.
--> get dagger
You pick up a dagger.
--> emote assume a fighting stance.
You assume a fighting stance.
--> attack guard
You hit a burly guard with a dagger.
You are hit by a burly guard with bare hands.
--> play jill
You already are Jill.
--> play jack
You are Jack. Jack is a fearsome warrior.
--> attack guard
You hit a burly guard with a wooden club.
--> quit
Bye!
|
parsexec.c |
- #include <ctype.h>
- #include <stdbool.h>
- #include <stdio.h>
- #include "object.h"
- #include "misc.h"
- #include "match.h"
- #include "location.h"
- #include "inventory.h"
- #include "inventory2.h"
- #include "openclose.h"
- #include "onoff.h"
- #include "talk.h"
- #include "attack.h"
- #include "social.h"
- typedef struct
- {
- const char *pattern;
- int (*function)(void);
- } COMMAND;
- static int executeQuit(void)
- {
- return -1;
- }
- static int executeNoMatch(void)
- {
- const char *src = *params;
- int len;
- for (len = 0; src[len] != '\0' && !isspace(src[len]); len++);
- if (len > 0) printf("I don't know how to '%.*s'.\n", len, src);
- return 0;
- }
- static int executeWait(void)
- {
- printf("Some time passes...\n");
- return 1;
- }
- int parseAndExecute(const char *input)
- {
- static const COMMAND commands[] =
- {
- { "quit" , executeQuit },
- { "look" , executeLookAround },
- { "look around" , executeLookAround },
- { "look at A" , executeLook },
- { "look A" , executeLook },
- { "examine A" , executeLook },
- { "go to A" , executeGo },
- { "go A" , executeGo },
- { "get A from B" , executeGetFrom },
- { "get A" , executeGet },
- { "put A in B" , executePutIn },
- { "drop A in B" , executePutIn },
- { "drop A" , executeDrop },
- { "ask A from B" , executeAskFrom },
- { "ask A" , executeAsk },
- { "give A to B" , executeGiveTo },
- { "give A" , executeGive },
- { "inventory" , executeInventory },
- { "open A" , executeOpen },
- { "close A" , executeClose },
- { "lock A" , executeLock },
- { "unlock A" , executeUnlock },
- { "turn on A" , executeTurnOn },
- { "turn off A" , executeTurnOff },
- { "turn A on" , executeTurnOn },
- { "turn A off" , executeTurnOff },
- { "talk with B about A" , executeTalkTo },
- { "talk about A with B" , executeTalkTo },
- { "talk about A" , executeTalk },
- { "talk A" , executeTalk },
- { "attack with B" , executeAttack },
- { "attack A with B" , executeAttack },
- { "attack A" , executeAttack },
- { "wait" , executeWait },
- { "play A" , executePlay },
- { "emote A" , executeEmote },
- { "say A" , executeSay },
- { "whisper to B A" , executeWhisper },
- { "A" , executeNoMatch }
- };
- const COMMAND *cmd;
- for (cmd = commands; !matchCommand(input, cmd->pattern); cmd++);
- return (*cmd->function)();
- }
|
The implementation of the new commands is extremely straightforward.
Of course, the ‘social’ commands are not very useful yet.
Being the only user, you are basically talking to yourself.
But it does give us the foundation to actually socialize in the next chapter.
social.h |
- extern int executePlay(void);
- extern int executeEmote(void);
- extern int executeSay(void);
- extern int executeWhisper(void);
|
social.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include "object.h"
- #include "match.h"
- #include "noun.h"
- #include "reach.h"
- #include "location.h"
- int executePlay(void)
- {
- OBJECT *obj = getTopic(params[0]);
- if (obj == NULL)
- {
- printf("I don't understand what character you want to play.\n");
- }
- else if (obj == player)
- {
- printf("You already are %s.\n", player->description);
- }
- else if (obj == jack || obj == jill)
- {
- player = obj;
- printf("You are %s. %s\n", player->description, player->details);
- }
- else
- {
- printf("That is not a character you can play.\n");
- }
- return 0;
- }
- int executeEmote(void)
- {
- const char *phrase = params[0];
- if (*phrase == '\0')
- {
- printf("I don't understand what you want to emote.\n");
- }
- else
- {
- printf("You %s\n", phrase);
- }
- return 0;
- }
- int executeSay(void)
- {
- const char *phrase = params[0];
- if (*phrase == '\0')
- {
- printf("I don't understand what you want to say.\n");
- }
- else
- {
- printf("You say: %s\n", phrase);
- }
- return 0;
- }
- int executeWhisper(void)
- {
- OBJECT *to = reachableObject("who to whisper to", params[1]);
- if (to != NULL)
- {
- const char *phrase = params[0];
- if (*phrase == '\0')
- {
- printf("I don't understand what you want to whisper.\n");
- }
- else
- {
- printf("You whisper to %s: %s\n", to->description, phrase);
- }
- }
- return 0;
- }
|
Explanation:
- Line 20:
here is a whitelist of viable player characters.
Do not forget to extend the list when introducing more player characters.
Alternatively, we could add a new attribute to OBJECT
to flag objects that can be impersonated by a user.
However, we cannot just assign a new value to player
(see line 22 above) without turning it into a variable.
object.awk |
- BEGIN {
- count = 0;
- obj = "";
- if (pass == "c2") {
- print "\nstatic bool alwaysTrue(void) { return true; }";
- print "\nOBJECT objs[] = {";
- }
- }
- /^- / {
- outputRecord(",");
- obj = $2;
- prop["condition"] = "alwaysTrue";
- prop["description"] = "NULL";
- prop["tags"] = "";
- prop["location"] = "NULL";
- prop["destination"] = "NULL";
- prop["prospect"] = "";
- prop["details"] = "\"You see nothing special.\"";
- prop["contents"] = "\"You see\"";
- prop["textGo"] = "\"You can't get much closer than this.\"";
- prop["gossip"] = "\"I know nothing about that.\"";
- prop["weight"] = "99";
- prop["capacity"] = "0";
- prop["health"] = "0";
- prop["light"] = "0";
- prop["impact"] = "0";
- prop["trust"] = "0";
- prop["open"] = "cannotBeOpened";
- prop["close"] = "cannotBeClosed";
- prop["lock"] = "cannotBeLocked";
- prop["unlock"] = "cannotBeUnlocked";
- }
- obj && /^[ \t]+[a-z]/ {
- name = $1;
- $1 = "";
- if (name in prop) {
- prop[name] = $0;
- if (/^[ \t]*\{/) {
- prop[name] = name count;
- if (pass == "c1") print "static bool " prop[name] "(void) " $0;
- }
- }
- else if (pass == "c2") {
- print "#error \"" FILENAME " line " NR ": unknown attribute '" name "'\"";
- }
- }
- !obj && pass == (/^#include/ ? "c1" : "h") {
- print;
- }
- END {
- outputRecord("\n}, *player = nobody;");
- if (pass == "h") {
- print "\n#define endOfObjs\t(objs + " count ")";
- print "\n#define validObject(obj)\t" \
- "((obj) != NULL && (*(obj)->condition)())";
- }
- }
- function outputRecord(separator)
- {
- if (obj) {
- if (pass == "h") {
- print "#define " obj "\t(objs + " count ")";
- }
- else if (pass == "c1") {
- print "static const char *tags" count "[] = {" prop["tags"] ", NULL};";
- }
- else if (pass == "c2") {
- print "\t{\t/* " count " = " obj " */";
- print "\t\t" prop["condition"] ",";
- print "\t\t" prop["description"] ",";
- print "\t\ttags" count ",";
- print "\t\t" prop["location"] ",";
- print "\t\t" prop["destination"] ",";
- print "\t\t" prop[prop["prospect"] ? "prospect" : "destination"] ",";
- print "\t\t" prop["details"] ",";
- print "\t\t" prop["contents"] ",";
- print "\t\t" prop["textGo"] ",";
- print "\t\t" prop["gossip"] ",";
- print "\t\t" prop["weight"] ",";
- print "\t\t" prop["capacity"] ",";
- print "\t\t" prop["health"] ",";
- print "\t\t" prop["light"] ",";
- print "\t\t" prop["impact"] ",";
- print "\t\t" prop["trust"] ",";
- print "\t\t" prop["open"] ",";
- print "\t\t" prop["close"] ",";
- print "\t\t" prop["lock"] ",";
- print "\t\t" prop["unlock"];
- print "\t}" separator;
- delete prop;
- }
- count++;
- }
- }
|
Explanation:
- Line 55:
player is now a pointer variable; it can be made to point to any object.
Initially, it will be pointing to object nobody, defined below.
Up until now, player has been a fixed object.
We will get rid of that object,
and instead introduce ‘named’ character objects
jack and jill.
object.txt |
- #include <stdbool.h>
- #include <stdio.h>
- #include "object.h"
- #include "toggle.h"
- typedef struct object {
- bool (*condition)(void);
- const char *description;
- const char **tags;
- struct object *location;
- struct object *destination;
- struct object *prospect;
- const char *details;
- const char *contents;
- const char *textGo;
- const char *gossip;
- int weight;
- int capacity;
- int health;
- int light;
- int impact;
- int trust;
- void (*open)(void);
- void (*close)(void);
- void (*lock)(void);
- void (*unlock)(void);
- } OBJECT;
- extern OBJECT *player, objs[];
- - heaven
- description "little heaven"
- tags "heaven", "little heaven"
- details "Everything looks so peaceful here."
- gossip "It's where all the good adventurers go."
- capacity 9999
- light 100
- - respawn
- description "a respawn portal"
- tags "portal", "respawn portal"
- location heaven
- destination field
- details "Looks like a gateway into the unknown."
- textGo "A bright flash of light, and you are back in the field."
- open isAlreadyOpen
- - heavenEWNS
- description "nothing but peace and quiet"
- tags "east", "west", "north", "south"
- location heaven
- textGo "In this dimension, there are no directions."
- gossip "It's just a compass direction."
- - waitingRoom
- description "the waiting room"
- tags "room", "waiting room"
- details "This is where everybody starts. Please enter: play YourName."
- gossip "It's where all the bad adventurers have to stay."
- capacity 9999
- light 100
- - waitingRoomWall
- description "walls all around you"
- tags "wall", "east", "west", "north", "south"
- location waitingRoom
- details "Written on the wall, it says: play Jack or play Jill."
- textGo "You cannot go anywhere."
- - nobody
- description "nobody"
- tags "nobody"
- location waitingRoom
- details "You are nobody. Please enter: play xxx"
- contents "You have"
- health 100
- capacity 1
- - field
- description "an open field"
- tags "field"
- details "The field is a nice and quiet place under a clear blue sky."
- gossip "A lot of tourists go there."
- capacity 9999
- light 100
- - cave
- description "a little cave"
- tags "cave"
- details "The cave is just a cold, damp, rocky chamber."
- gossip "It's dark in there; bring a lamp!"
- capacity 9999
- - silver
- description "a silver coin"
- tags "silver", "coin", "silver coin"
- location field
- details "The coin has an eagle on the obverse."
- gossip "Money makes the world go round..."
- weight 1
- - gold
- description "a gold coin"
- tags "gold", "coin", "gold coin"
- location openBox
- details "The shiny coin seems to be a rare and priceless artefact."
- gossip "Money makes the world go round..."
- weight 1
- - guard
- description "a burly guard"
- tags "guard", "burly guard"
- location field
- details "The guard is a really big fellow."
- gossip "Easy to bribe..."
- contents "He has"
- health 100
- impact -1
- capacity 20
- - jack
- description "Jack"
- tags "jack", "warrior", "man"
- location field
- details "Jack is a fearsome warrior."
- gossip "Jack is a fearsome warrior."
- contents "Jack has"
- health 100
- impact -1
- capacity 20
- - jill
- description "Jill"
- tags "jill", "valkyrie", "woman"
- location field
- details "Jill is a vicious valkyrie."
- gossip "Jill is a vicious valkyrie."
- contents "Jill has"
- health 100
- impact -1
- capacity 20
- - intoCave
- condition { return guard->health == 0 || silver->location == guard; }
- description "a cave entrance to the east"
- tags "east", "entrance"
- location field
- destination cave
- details "The entrance is just a narrow opening in a small outcrop."
- textGo "You walk into the cave."
- open isAlreadyOpen
- - intoCaveBlocked
- condition { return guard->health > 0 && silver->location != guard; }
- description "a cave entrance to the east"
- tags "east", "entrance"
- location field
- prospect cave
- details "The entrance is just a narrow opening in a small outcrop."
- textGo "The guard stops you from walking into the cave."
- open isAlreadyOpen
- - exitCave
- description "an exit to the west"
- tags "west", "exit"
- location cave
- destination field
- details "Sunlight pours in through an opening in the cave's wall."
- textGo "You walk out of the cave."
- open isAlreadyOpen
- - wallField
- description "dense forest all around"
- tags "west", "north", "south", "forest"
- location field
- details "The field is surrounded by trees and undergrowth."
- textGo "Dense forest is blocking the way."
- gossip "You cannot go there, it is impenetrable."
- - wallCave
- description "solid rock all around"
- tags "east", "north", "rock"
- location cave
- details "Carved in stone is a secret password 'abccb'."
- textGo "Solid rock is blocking the way."
- - backroom
- description "a backroom"
- tags "backroom"
- details "The room is dusty and messy."
- gossip "There is something of value to be found there."
- capacity 9999
- - wallBackroom
- description "solid rock all around"
- tags "east", "west", "south", "rock"
- location backroom
- details "Trendy wallpaper covers the rock walls."
- textGo "Solid rock is blocking the way."
- - openDoorToBackroom
- description "an open door to the south"
- tags "south", "door", "doorway"
- destination backroom
- details "The door is open."
- textGo "You walk through the door into a backroom."
- open isAlreadyOpen
- close toggleDoorToBackroom
- - closedDoorToBackroom
- description "a closed door to the south"
- tags "south", "door", "doorway"
- location cave
- prospect backroom
- details "The door is closed."
- textGo "The door is closed."
- open toggleDoorToBackroom
- close isAlreadyClosed
- - openDoorToCave
- description "an open door to the north"
- tags "north", "door", "doorway"
- destination cave
- details "The door is open."
- textGo "You walk through the door into the cave."
- open isAlreadyOpen
- close toggleDoorToCave
- - closedDoorToCave
- description "a closed door to the north"
- tags "north", "door", "doorway"
- location backroom
- prospect cave
- details "The door is closed."
- textGo "The door is closed."
- open toggleDoorToCave
- close isAlreadyClosed
- - openBox
- description "a wooden box"
- tags "box", "wooden box"
- details "The box is open."
- gossip "You need a key to open it."
- weight 5
- capacity 10
- open isAlreadyOpen
- close toggleBox
- lock isStillOpen
- unlock isAlreadyOpen
- - closedBox
- description "a wooden box"
- tags "box", "wooden box"
- details "The box is closed."
- weight 5
- open toggleBox
- close isAlreadyClosed
- lock toggleBoxLock
- unlock isAlreadyUnlocked
- - lockedBox
- description "a wooden box"
- tags "box", "wooden box"
- location backroom
- details "The box is closed."
- weight 5
- open isStillLocked
- close isAlreadyClosed
- lock isAlreadyLocked
- unlock toggleBoxLock
- - keyForBox
- description "a tiny key"
- tags "key", "tiny key"
- location cave
- details "The key is really small and shiny."
- gossip "A small key opens a small lock."
- weight 1
- - lampOff
- description "a lamp"
- tags "lamp"
- location field
- details "The lamp is off."
- gossip "Essential in dark areas."
- weight 5
- - lampOn
- description "a lamp"
- tags "lamp"
- details "The lamp is on."
- weight 5
- light 100
- - club
- description "a wooden club"
- tags "club", "wooden club"
- location field
- details "Two feet of solid wood."
- weight 5
- impact -2
- - dagger
- description "a dagger"
- tags "dagger"
- location field
- details "The dagger is very sharp."
- weight 7
- impact -5
|
Explanation:
- Line 29:
we are exposing variable player
to every module that includes object.h.
- Line 55-76:
until a user has specified for the first time
which character they will be playing (with command ‘play’),
they will be ‘nobody’.
Nobody is a dummy player character, locked up in a waiting room,
waiting for the user to switch over to a different player character.
- Line 120-140:
initially, Jack and Jill are both located in the field,
but of course it is also possible to let each one start in different locations.
Traditionally, descriptive text about the player is always in second
person,
e.g. “You are in an open field.”
But with multiple player characters, that is no longer the case.
Second person is still appropriate for
the player character that is currently under the user’s control,
but any other player characters you meet, should be referenced in third person.
This is already giving trouble in command inventory.
The game’s response has always started with “You have”,
as specified by attribute contents of the player object.
But for Jack and Jill, things are not so straightforward;
contents is used not only to inspect yourself (inventory),
but also to inspect someone else (e.g. Jill enters: examine Jack).
We will just keep it simple for now:
if the object being inspected is currently under the user’s control,
then we will output a hardcoded “You have”
rather than attribute contents.
misc.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include "object.h"
- #include "misc.h"
- bool isHolding(OBJECT *container, OBJECT *obj)
- {
- return validObject(obj) && obj->location == container;
- }
- bool isLit(OBJECT *target)
- {
- OBJECT *obj;
- if (validObject(target))
- {
- if (target->light > 0)
- {
- return true;
- }
- for (obj = objs; obj < endOfObjs; obj++)
- {
- if (validObject(obj) && obj->light > 0 &&
- (isHolding(target, obj) || isHolding(target, obj->location)))
- {
- return true;
- }
- }
- }
- return false;
- }
- static bool isNoticeable(OBJECT *obj)
- {
- return obj->location == player ||
- isLit(obj) || isLit(obj->prospect) || isLit(player->location);
- }
- OBJECT *getPassage(OBJECT *from, OBJECT *to)
- {
- if (from != NULL && to != NULL)
- {
- OBJECT *obj;
- for (obj = objs; obj < endOfObjs; obj++)
- {
- if (isHolding(from, obj) && obj->prospect == to)
- {
- return obj;
- }
- }
- }
- return NULL;
- }
- DISTANCE getDistance(OBJECT *from, OBJECT *to)
- {
- return to == NULL ? distUnknownObject :
- !validObject(to) ? distNotHere :
- to == from ? distSelf :
- isHolding(from, to) ? distHeld :
- !isNoticeable(to) ? distNotHere :
- isHolding(to, from) ? distLocation :
- isHolding(from->location, to) ? distHere :
- isHolding(from, to->location) ? distHeldContained :
- isHolding(from->location, to->location) ? distHereContained :
- getPassage(from->location, to) != NULL ? distOverthere :
- distNotHere;
- }
- OBJECT *actorHere(void)
- {
- OBJECT *obj;
- for (obj = objs; obj < endOfObjs; obj++)
- {
- if (isHolding(player->location, obj) && obj != player &&
- isNoticeable(obj) && obj->health > 0)
- {
- return obj;
- }
- }
- return NULL;
- }
- int listObjectsAtLocation(OBJECT *location)
- {
- int count = 0;
- OBJECT *obj;
- for (obj = objs; obj < endOfObjs; obj++)
- {
- if (obj != player && isHolding(location, obj) && isNoticeable(obj))
- {
- if (count++ == 0)
- {
- printf("%s:\n", location == player ? "You have"
- : location->contents);
- }
- printf("%s\n", obj->description);
- }
- }
- return count;
- }
|
This was all fairly simple; it did not require a whole lot of source changes.
In the next chapter, we will be writing a lot more code,
as we are diving into the wonderful world of socket programming.
Next chapter: 22. Client-server