Contents
19. Conversations
|
How to program a text adventure in C
by Ruud Helderman
<r.helderman@hccnet.nl>
Licensed under
MIT License
19. Conversations
One of the most difficult parts of writing an adventure
is to come up with challenging puzzles.
Puzzles must be difficult, but without being illogical.
If your game demands the player to unlock a door with a towel, fine.
But then at least give the player a clue.
It’s no fun brute-forcing your way through a game,
having to try every combination of object(s) and verb until you find
something that matches the author’s obscure sense of logic.
Clues can be hidden in descriptive text.
This text may come out unannounced,
for example as you enter a location
(“A hollow voice says
PLUGH”)
or run into an actor.
But it also makes sense to let the player actively search for clues
by examining objects and talking to non-player characters.
Obvious advantages are:
- It gives the player a sense of being actively involved in the story.
In a detective story,
examining the crime scene and interrogating witnesses and suspects
should play a vital role in solving the mystery.
- It is a natural way to split descriptive text into smaller chunks,
and serving the information in an on-demand fashion to the player.
This prevents a waterfall of (unsolicited) information
at certain (fixed) points in the game.
Conversation with non-player characters can add a lot of depth to a game.
Clues given may help you or hinder you.
Some examples:
- When entering a town, the locals may be able to tell you
how to earn money, where to find food, or how to sneak into the castle.
The more focused the inquiry
(referring to a relevant item, location or actor),
the more focused the information returned.
Like in real life, it’s all about asking the right questions.
- Information gathered from different sources
may be interrelated or contradictory.
The baker may have given you some useful gossip,
but what if the goldsmith told you the baker is actually a notorious liar?
- The player may need to ‘encourage’ actors to spill their guts.
Either with a friendly gesture (is there some item they need?) or by force.
An actor’s willingness to supply (valid) information
could be implemented by certain ‘psychological’ properties,
rating their loyalty or fear towards the player.
Conversations may go a lot further than just asking for information.
Some suggestions:
- Allow the player to give commands to other actors,
for example: “Tell giant to break door with axe.”
So rather than taking action yourself,
you could try and find a ‘specialist’ to do the job.
Whether or not other actors will obey your commands, might depend
on the effort you have put into building a certain relationship with them.
- Allow the player to pass information back to other actors.
You may not be strong enough to push aside a pair of guards,
but it might help to use a little lie to spark a conflict between them.
Of course, these more advanced forms of interacting with actors
demand a pretty sophisticated conversation engine.
A recommended read is
Dynamic Conversation Engine Concept by Luke Bergeron.
I found this to be incredibly inspiring.
The article has been removed from
Scribd,
but fortunately the
Wayback Machine
still has a copy:
https://web.archive.org/web/20100409073709/http://www.scribd.com/doc/17682546/Dynamic-Conversation-Engine-Concept
Talk
In our little adventure game, we will try to keep it simple.
We will implement a very straightforward ‘talk’ verb.
It allows the player to talk with an actor about some topic.
The topic of discussion can be any object:
an item, a location, even another actor.
In its most elaborate form,
you could give every actor their own opinion about certain topics.
That requires a matrix of possible reactions;
one for every combination of actor and object.
|
Tour guide |
Guard |
Cave |
Silver coin |
Gold coin |
Tour guide |
Guard |
In a big adventure, such a matrix could be huge,
making it extremely time-consuming for the author to come up with
a witty response on every attempt from the player to engage in conversation.
Furthermore, a huge number of combinations
could easily turn into just another brute-force puzzle, where the player
feels forced to interrogate every actor about every possible topic.
To keep it maintainable,
you may want to cut down on the number of combinations.
Here are a few ways to do so.
- Eliminate actors from the matrix.
Some actors may be unable to speak (e.g. animals),
not willing to speak (enemies), or both (monsters).
Those actors do not belong in the matrix;
a single uniform response per actor should suffice.
(“The dog replies: woof woof woof.”)
An exceptional response to one particular topic
could be handled in a hard-coded way.
(“When hearing the word ‘rosebud’,
the dog starts to growl.”)
- Eliminate objects from the matrix.
For many (or even all) objects, you may want to supply a single response,
to be returned by every (talkative) actor.
(“You get the advice: do not fight with the guard!”)
Again, if there is an exceptional answer, to be given by a particular actor,
then that can be hard-coded.
(“The tour guide says: do not forget to pay the guard!”)
- Group actors.
Instead of giving every individual its own column in the matrix,
use ‘categories’ of actors.
There may be many different villagers,
but if they all share the same basic knowledge about the majority of topics,
then this will save you a lot of duplicate entries.
Again, exceptions to the rule can be hard-coded.
- Multiple smaller matrices.
Suppose the game is set in a big world
where the player roams from village to village.
Villagers may not be aware of items, locations and actors in other villages.
Giving each village its own matrix, eliminates all the irrelevant cross-village combinations.
(A mathematician might refer to this approach as a
block diagonal matrix.)
- Sparse matrix.
All the previous approaches had one disadvantage:
there is no structural solution for exceptional combinations of actor and object
(that one actor to give the player the essential clue about that one object).
But there is an alternative to 'hard-coding' the exceptions:
put these combinations in a separate
sparse matrix.
Such a matrix would typically be implemented not as a two-dimensional array,
but as a list of tuples.
Covering exceptional cases only, the list should be relatively short,
which helps to keep it compact and maintainable.
TODO: properties 'loyalty' and 'confidentiality'.
Only spill gossip if object's confidentiality < actor's loyalty.
To keep things simple, we will stick to a single response per object,
given by any actor the player will talk to about that object.
Sample output |
Welcome to Little Cave Adventure.
You are in an open field.
You see:
a silver coin
a burly guard
a cave entrance to the east
dense forest all around
a lamp
--> talk with guard
I don't understand what you want to talk about.
--> talk about coin
You hear a burly guard say: 'Money makes the world go round...'
--> talk with guard about forest
You hear a burly guard say: 'You cannot go there, it is impenetrable.'
--> talk about cave
You hear a burly guard say: 'It's dark in there; bring a lamp!'
--> talk about east
You hear a burly guard say: 'It's just a compass direction.'
--> talk about yourself
You hear a burly guard say: 'You're not from around here, are you?'
--> talk about guard
You hear a burly guard say: 'I don't want to talk about myself.'
--> talk about hamburgers
I don't understand what you want to talk about.
--> talk
I don't understand what you want to talk about.
--> quit
Bye!
|
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;
- void (*open)(void);
- void (*close)(void);
- void (*lock)(void);
- void (*unlock)(void);
- } OBJECT;
- extern OBJECT objs[];
- - gossipEWNS
- tags "east", "west", "north", "south"
- gossip "It's just a compass direction."
- - 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
- capacity 20
- - player
- description "yourself"
- tags "yourself"
- location field
- details "You would need a mirror to look at yourself."
- gossip "You're not from around here, are you?"
- contents "You have"
- health 100
- 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
|
Explanation:
- Line 16:
the new property.
- Line 29-31:
introducing a dummy object to prevent generic tags
(in this case east, west, north, south) to be handled by a specific object
(for example the forest).
As always, we also need to make object.awk aware of the new property,
and specify a default value.
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["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};");
- 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["open"] ",";
- print "\t\t" prop["close"] ",";
- print "\t\t" prop["lock"] ",";
- print "\t\t" prop["unlock"];
- print "\t}" separator;
- delete prop;
- }
- count++;
- }
- }
|
Adding a new command ‘talk’ to the parser:
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"
- typedef struct
- {
- const char *pattern;
- bool (*function)(void);
- } COMMAND;
- static bool executeQuit(void)
- {
- return false;
- }
- static bool 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 true;
- }
- bool 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 },
- { "A" , executeNoMatch }
- };
- const COMMAND *cmd;
- for (cmd = commands; !matchCommand(input, cmd->pattern); cmd++);
- return (*cmd->function)();
- }
|
Explanation:
- Line 64-67:
four new patterns for one new verb.
As explained in chapter 14, the order of the patterns is important.
As many times before, we introduce a new module to implement the new command:
talk.h |
- extern bool executeTalk(void);
- extern bool executeTalkTo(void);
|
talk.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include "object.h"
- #include "misc.h"
- #include "match.h"
- #include "noun.h"
- #include "reach.h"
- static void talk(const char *about, OBJECT *to)
- {
- OBJECT *topic = getTopic(about);
- if (topic == NULL)
- {
- printf("I don't understand what you want to talk about.\n");
- }
- else
- {
- printf("You hear %s say: '%s'\n",
- to->description,
- topic == to ? "I don't want to talk about myself."
- : topic->gossip);
- }
- }
- bool executeTalk(void)
- {
- OBJECT *to = actorHere();
- if (to != NULL)
- {
- talk(params[0], to);
- }
- else
- {
- printf("There is nobody here to talk to.\n");
- }
- return true;
- }
- bool executeTalkTo(void)
- {
- OBJECT *to = reachableObject("who to talk to", params[1]);
- if (to != NULL)
- {
- if (to->health > 0)
- {
- talk(params[0], to);
- }
- else
- {
- printf("There is no response from %s.\n", to->description);
- }
- }
- return true;
- }
|
Function getTopic is implemented in one of the existing modules:
noun.h |
- extern OBJECT *getVisible(const char *intention, const char *noun);
- extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun);
- extern OBJECT *getTopic(const char *noun);
|
noun.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include <string.h>
- #include "object.h"
- #include "misc.h"
- static bool objectHasTag(OBJECT *obj, const char *noun)
- {
- if (noun != NULL && *noun != '\0')
- {
- const char **tag;
- for (tag = obj->tags; *tag != NULL; tag++)
- {
- if (strcmp(*tag, noun) == 0) return true;
- }
- }
- return false;
- }
- static OBJECT ambiguousNoun;
- static OBJECT *getObject(const char *noun, OBJECT *from, DISTANCE maxDistance)
- {
- OBJECT *obj, *res = NULL;
- for (obj = objs; obj < endOfObjs; obj++)
- {
- if (objectHasTag(obj, noun) && getDistance(from, obj) <= maxDistance)
- {
- res = res == NULL ? obj : &ambiguousNoun;
- }
- }
- return res;
- }
- OBJECT *getVisible(const char *intention, const char *noun)
- {
- OBJECT *obj = getObject(noun, player, distOverthere);
- if (obj == NULL)
- {
- if (getObject(noun, player, distNotHere) == NULL)
- {
- printf("I don't understand %s.\n", intention);
- }
- else if (isLit(player->location))
- {
- printf("You don't see any %s here.\n", noun);
- }
- else
- {
- printf("It's too dark.\n");
- }
- }
- else if (obj == &ambiguousNoun)
- {
- printf("Please be specific about which %s you mean.\n", noun);
- obj = NULL;
- }
- return obj;
- }
- OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun)
- {
- OBJECT *obj = NULL;
- if (from == NULL)
- {
- printf("I don't understand who you want to %s.\n", verb);
- }
- else if ((obj = getObject(noun, from, distHeldContained)) == NULL)
- {
- if (getObject(noun, player, distNotHere) == NULL)
- {
- printf("I don't understand what you want to %s.\n", verb);
- }
- else if (from == player)
- {
- printf("You are not holding any %s.\n", noun);
- }
- else
- {
- printf("There appears to be no %s you can get from %s.\n",
- noun, from->description);
- }
- }
- else if (obj == &ambiguousNoun)
- {
- printf("Please be specific about which %s you want to %s.\n",
- noun, verb);
- obj = NULL;
- }
- else if (obj == from)
- {
- printf("You should not be doing that to %s.\n", obj->description);
- obj = NULL;
- }
- return obj;
- }
- OBJECT *getTopic(const char *noun)
- {
- OBJECT *obj;
- for (obj = objs; obj < endOfObjs; obj++)
- {
- if (objectHasTag(obj, noun)) return obj;
- }
- return NULL;
- }
|
Explanation:
- Line 98-106:
‘talk’ is one of the few commands that can be applied to an object
that is not present; that you may not even have seen yet. Therefore,
function getTopic is not interested in the object’s distance.
It will return whatever object it can find with the given tag.
And if you cannot talk your way out of a difficult situation,
then there is always the option to use physical force.
More on that in the following chapter.
Next chapter: 20. Combat