Contents7. Distance
|
by Ruud Helderman <r.helderman@hccnet.nl>
Licensed under MIT License
A typical adventure contains many puzzles. Infocom adventures were notoriously difficult to complete; solving every puzzle might require weeks, even months of trial and error. Don’t be surprised if the majority of replies from the game are ‘errors’: you can’t do this; you can’t go there; you died. I hate adventures that just return “You can’t” in response to every attempt I make to solve one of the game’s puzzles. Such a reply is dull and not very helpful. It neglects an important aspect of any computer game; in fact, an essential part of life itself: the player has to learn from his mistakes. It is OK for an adventure to be difficult, even frustratingly difficult. But when the player has the feeling he is not making any progress at all, or when the only way to make progress is a brute-force attack on all verb-noun combinations, then even the most hardened player will lose interest and eventually give up. The least an adventure game should do, is explain why the player’s command cannot be completed: “You can’t do that, because...” This helps to make the virtual world more convincing, the story more credible, and the game more enjoyable.
I already put quite some effort in having the game explain why certain commands are ineffective. Just take a look at the many printf calls in noun.c, inventory.c, location.c, move.c. But as the game is getting increasingly complex, this is becoming quite a burden. I need a more structural way to detect and handle error conditions. That is what we will be working on in this chapter.
Most commands operate on one or more objects, for example:
The first thing to check (after the obvious typos caught by the parser) is for the presence of these objects; failure should result in something like “There is no ... here” or “You don’t see any ...” In this chapter, we will build a generic function that can be used by every command to find out if an object is within reach of the player.
You may think we only need to distinguish two cases: either the object is here, or it is not. But many commands require more gradients than just ‘here’ and ‘not here’. Examples:
It all boils down to the fact that there are different notions of ‘here’:
distSelf | The object is the player | object == player |
distHeld | The player is holding the object | object->location == player |
distHeldContained | The player is holding another object (for example a bag) containing the object | object->location != NULL && object->location->location == player |
distLocation | The object is the player’s location | object == player->location |
distHere | The object is present at the player’s location | object->location == player->location |
distHereContained | Another object (either an actor or a ‘container’), present at the player’s location, is holding (but not hiding) the object | object->location != NULL && object->location->location == player->location |
distOverthere | The object is a nearby location | getPassage(player->location, object) != NULL |
The first case (object is player) may seem trivial, but it is important nonetheless. For example, the command "examine yourself" should not return "There is no yourself here."
I tried to follow a logical order: nearby things are at the top, further down below they become more distant. We can continue the list, to cover objects that are even further away:
distNotHere | The object is (or appears to be) not here | |
distUnknownObject | The parser did not recognize the noun entered | object == NULL |
Notice we have seven different cases of ‘here’, but only one for ‘not here’. This is because typically, the game only needs to provide information about things that can be perceived by the player. If it’s not here, then there’s nothing more to say.
In the leftmost column, I proposed a symbolic name for each case. We will gather these names in an enum named DISTANCE.
typedef enum { distSelf, distHeld, distHeldContained, distLocation, distHere, distHereContained, distOverthere, distNotHere, distUnknownObject } DISTANCE; |
And in the rightmost column, I proposed a condition for each case to satisfy. With a little reshuffling, we can easily turn this into a function that calculates the ‘distance’ of an object (as seen from the player’s point of view):
DISTANCE getDistance(OBJECT *from, OBJECT *to) { return to == NULL ? distUnknownObject : to == from ? distSelf : to->location == from ? distHeld : to == from->location ? distLocation : to->location == from->location ? distHere : getPassage(from->location, to) != NULL ? distOverthere : to->location == NULL ? distNotHere : to->location->location == from ? distHeldContained : to->location->location == from->location ? distHereContained : distNotHere; } |
That’s all! We can call this function and do a comparison on its return value. For example, we had the following piece of code in noun.c:
else if (!(obj == player || obj == player->location || obj->location == player || obj->location == player->location || getPassage(player->location, obj) != NULL || (obj->location != NULL && (obj->location->location == player || obj->location->location == player->location)))) |
We can now replace each sub-condition by an appropriate distance check:
else if (!(getDistance(player, obj) == distSelf || getDistance(player, obj) == distLocation || getDistance(player, obj) == distHeld || getDistance(player, obj) == distHere || getDistance(player, obj) == distOverthere || getDistance(player, obj) == distHeldContained || getDistance(player, obj) == distHereContained) |
This can be reduced to:
else if (getDistance(player, obj) >= distNotHere) |
This was just an example to give you an idea of the concept; the actual implementation of noun.c you will find below, looks slightly different.
Time to put things into place. The definitions of enum DISTANCE and function getDistance are added to misc.h and misc.c, since we will be using them in more than one module.
misc.h |
---|
|
misc.c |
|
Explanation:
In function executeGo, we can replace most of the if conditions by a check on distance.
Sample output |
---|
Welcome to Little Cave Adventure.
You are in an open field. You see: a silver coin a burly guard a cave entrance --> go guard You can't get much closer than this. --> give silver You are not holding any silver. --> ask silver There appears to be no silver you can get from a burly guard. --> get silver You pick up a silver coin. --> get gold You don't see any gold here. --> give silver You give a silver coin to a burly guard. --> go cave OK. You are in a little cave. You see: a gold coin an exit --> get gold You pick up a gold coin. --> give gold There is nobody here to give that to. --> quit Bye! |
location.h |
---|
|
location.c |
|
The same goes for function executeGet.
inventory.h |
---|
|
inventory.c |
|
And finally, we will adjust the constraints in noun.c. I am adding two parameters to function getObject that make it possible to find a match for a specific noun, while at the same time ignore any objects that are considered absent. This will really pay off in the next chapter, where we will introduce different objects having the same tag.
noun.h |
---|
|
noun.c |
|
Explanation:
The other modules (main.c, parsexec.c, move.c, object.c) remain unchanged, you can see them in previous chapters.
In this chapter, the concept of distance was mainly used to choose between different responses the game can give to the user. But the benefits of distance are not reserved to the output side; it can be used equally well to make improvements on the input side. In the next chapter, we will use distance to improve the recognition of nouns entered by the user.
⭳ Download source code 🌀 Run on Repl.it |
Next chapter: 8. North, east, south, west