Challenge, guide, and tease—but don’t frustrate—your players.
IF games are often also called adventure games. Ask Infocom aficionados about their favorite moments, and they might mention the diamond puzzle in Zork II or the Babelfish puzzle from The Hitchhiker’s Guide to the Galaxy. Conflict is the heart of drama. Unless you’re creating an experimental new form of interactive fiction, you need to introduce some sort of conflict for the player to experience and overcome.
The canonical way to do that is to add a puzzle.
Let’s add a puzzle to the Inform adventure created in [Hack #85] and decorated in [Hack #86] .
The meat of the puzzle lies in the Scanner room.
Here, the player must adjust both the analysis nets and the eye
catalog—the lock and the key—to match each other. The
nets then open, releasing an identity token. The player can take the
token, hand it to the access controller (back in the
Router), and be on his way.
To support these actions, you’ll need an analysis
nets object and a way to make it change color (as the catalog does).
For variety’s sake, let’s implement
this as two game objects: the nets and a separate
selector that changes the color setting:
Object nets "analysis nets" Scanner,
with
name 'array' 'arrays' 'image' 'analysis' 'image-analysis' 'net' 'nets',
description [;
print "Arrays of image-analysis nets hang around the sensor
fountain, ready to accept and identify eyeprint data, should
any arrive. A ";
selector.colorname( );
" identity selector hangs off the nets.";
],
has scenery container open pluralname;
Object selector "identity selector" Scanner,
with
name 'id' 'identity' 'selector',
article "an",
color 0,
colorname [;
switch (self.color) {
0: print "chartreuse";
1: print "crimson";
2: print "topaz";
3: print "copper";
4: print "pink";
}
],
description [;
print "The selector indicates which meat-person the retinal
scanner is trying to identify. Normally it would
adjust itself to the datastream coming through the
scanner -- but you can probably move it yourself, from
in here. The selector is currently ";
self.colorname( );
".";
],
before [;
Turn:
self.color++;
if (self.color >= 5)
self.color = 0;
print "Click. The identity selector turns ";
self.colorname( );
".";
Push, Pull, Set:
<<Turn self>>;
],
has scenery;Variety is not the sole reason to separate the objects. The nets,
unlike the catalog, aren’t part of the
spy’s toolkit. They are unfamiliar. You must still
clue the player in on how to change the net’s color,
but try to convey this information as an observation, not an
ingrained habit. Therefore, the action must be straightforward. By
bringing a distinct selector to the
player’s attention, you invite experimentation. Any
selector will have a way to select.
Most of this is familiar. The nets have the
container and open attributes.
The standard library can handle actions appropriate to a container
(look in
nets, put something in nets, take something from nets, and so on). The container
attribute ordains these, and open indicates that
the container is open, so you can see inside.
For once, you don’t need article
"an" on the analysis nets. This is because the
nets also have pluralname, and
so the library refers to them as some
analysis nets.
The selector has color and
colorname properties, just like those of the
catalog, though there are only five colors. (There
are five authorized meat-people for this retinal scanner, no doubt.)
Notice that the nets.description function invokes
selector.colorname(); you can see the
selector’s color no matter which of the two parts
you examine.
The selector responds to a different action; you
don’t Squeeze it, you
Turn it. Let’s be generous about
this, though, because you want your spy to realize how to use this
device; you certainly don’t want the player fumbling
around, trying to guess the right command. So you accept
Push, Pull, and
Set actions as well. These three cases execute
<<Turn self>>. This statement begins a
new action and then returns true so that the previous action ends.
The preceding case catches the new Turn action,
and so all four commands become synonyms.
It’s time to create the identity token, which is the goal of this whole scene. It’s a straightforward object:
Object token "identity token" nets,
with
name 'id' 'identity' 'token',
article "an",
description "This denotes an identity within the system, validated
by biometric data. (Why can't meat-people just have built-in
crypto-keys? You've never understood it.)";The token begins inside the
nets. The challenge is to remove it. However, the
player won’t know
there’s a challenge unless she sees the token.
It’s not listed in the room description.
It’s present: you can examine it and even try
look in nets, but that’s not good
enough. The situation must be clear, or the player
won’t ever know that she’s missing
an object.
Let’s do this the simplest possible way by adding
code to the room description. Change the
Scanner’s description:
Object Scanner "Scanner South-9",
has light,
with
description [;
print "The firmware here is tight and clean; this device, at
least, is well-maintained. You can see image-analysis nets
strung in their neat rows around the sensor fountain which
dominates this space. The only exit is the serial line to the
north.^";
if (token in nets)
print "^You see an identity token caught in the image nets.^";
rtrue;
],
n_to Router;The first paragraph of text is the same, except that, because this is
a print statement, you need an explicit line
break. Then, if the token is still within the
nets, you print an additional line. This has a
line break before and after to conform to Inform’s
formatting rules, which mandate a blank line between paragraphs.
This simple room description hack is sufficient,
because in this game, the nets contain either the
token or nothing. If there were several objects in
there, you’d need a series of
print statements—tedious to write and also
hard for the player to read. In that case, it’s
better to use the library’s
WriteListFrom function to write an arbitrary list
of contents as a nice grammatical sentence.
If you’ve compiled the current version of the code,
you’ve probably noticed the next flaw: taking the
identity token is easy! Nothing prevents the player from typing
take
token and walking
away with it, without ever solving the color-matching lock-and-key
puzzle at all.
You can fix this several ways. You could customize the
token’s Take action, for example.
However, that actually leaves loopholes open. There are several
library actions that can extract an object from a container:
Take is only the most common.
Conveniently, all these extraction actions check the
container’s LetGo action first.
(The container really is the best place for this code, anyway. If
there were several identity tokens in the nets,
you’d want to write a single
LetGo test on the nets object,
rather than having to modify the Take action of
every token.)
So, add these two properties to the nets object:
accessed false,
before [;
LetGo:
if (self.accessed = = false)
"The nets have not validated a retinal pattern; they
refuse to yield the token to you.";
],The accessed property is another custom job, not
defined by the library. You can define it however you like.
It’s best, of course, as a logical value that tells
whether the nets are open.
The LetGo action does not result directly from a
player command, as do Take,
Turn, and Squeeze. It is part
of any action that removes something from a container. Nonetheless,
you can customize it the same way: in this before
property, check the accessed value; if
it’s not true, print a grim refusal. As usual, the
bare-string statement returns true, ending the entire action. If
self.accessed is true, the
LetGo case ends, returning the default false
value, and you’ll see the default behavior: the game
allows the player to take the token.
Now you just need the command with which the player actually opens the analysis net, using his handy eye catalog.
What command should this be? Several possibilities might make sense.
You don’t want it to be too hard to guess, so
let’s take a straightforward command:
put
catalog
in
nets. This captures the idea
that you’re inserting false data into the analysis
network.
Again, you can customize the Insert action of the
token, but it’s cleaner to customize the
Receive action of the nets
instead. Receive is the converse of
LetGo; it is part of any action that places an
object in (or on) a container. (By putting the code in
Receive, you’ll recognize
put catalog on nets as well as put catalog in nets. That’s good.)
Extend the nets.before property:
before [;
Receive:
if (noun = = catalog) {
if (self.accessed) {
"The nets have already validated your identity.
Well, not YOUR identity, but somebody's.";
}
print "A wave of ";
catalog.colorname( );
print " data flows into the analysis nets. They absorb
it hungrily";
if (catalog.color = = 3 && selector.color = = 1) {
self.accessed = true;
"... and then untangle themselves, unveiling
the identity token. It is now free for
the taking.";
}
else {
"... and then tear it to monochrome
meaningless bits.";
}
}
"The nets do not accept your offer.";
LetGo:
if (self.accessed = = false)
"The nets have not validated a retinal pattern; they
refuse to yield the token to you.";
],This is the longest block of code yet; it has a lot of work to do.
The only object the nets accept is the
catalog. If the player tries to insert anything
else, the Receive case skips down to its last
line, rejecting the offer. The bare string prints and returns.
Let’s take the opportunity to add some
anthropomorphism; the net description makes them sound as if
they’re sentient. Are they? Ambiguity is all part of
the fun.
The Receive action has to discriminate what the
nets receive. For this, check the
noun global variable. During a
before routine, noun holds the
object of the action (and second holds the
secondary object, if there is one).
Within the if (noun
= =
catalog) case, first check the code if the puzzle
has already been solved. Once you open the nets
(and accessed is true), you don’t
want the player to repeat the action. Another bare string returns
true.
The nets untangle themselves again.Plus, what happens if the player enters a wrong combination after the right one? Does the net lock away the token again? The puzzle would have more solidity if you added all this code, but in the interest of brevity, let’s just block the player from even trying to repeat the action. This doesn’t feel unduly restrictive, because once the player has the token, she’ll be more eager to use it than to keep playing around with the retinal scanner.
Now you know the player really has used the
catalog on the nets. Print the
first part of the message:
A wave of bright gold data flows into the analysis nets. They absorb it hungrily.
Of course, you call catalog.colorname to discover
the catalog’s current color; that’s
the fraudulent data the player is inserting into the analysis stream.
The second print statement ends with
hungrily, with no punctuation or space following
it. This is handy, because you can follow it up with anything: a
period, comma, ellipsis, or and.
There are two possible cases. Either the player has chosen the right
colors or she hasn’t. The catalog’s
list of six colors and the analysis nets’ list of
five overlap just once, at crimson. If
catalog.color is 3, and
selector.color is 1, there’s a
match. Set the accessed flag to true, and print a
triumphant message. Otherwise, indicate failure. (The failure message
slyly hints that color is significant.)
That covers everything. Note that every case ends with a bare-string statement, which implicitly returns true to stop the action.
The Scanner room needs one more feature. The room
description mentions the sensor fountain, so you should certainly
implement it. On the other hand, the fountain has no purpose in the
game. Its purpose in the game world is to digitize an actual retinal
print and transfer it to the analysis nets, but no actual humans will
wander by during the scene.
Let’s add a description. Do you need to react to any
commands? Yes. When the player decides to introduce his
catalog’s data into the retinal scanner, he might
type put sphere in fountain. That makes just as
much sense as put
sphere in nets; after all, the whole point of the fountain is to send
data to the nets.
You can do this by duplicating the code from the
Receive action of the nets, but
it’s easier to generate a new action using the
statement <<Insert catalog nets>>;
this behaves just as if the player had typed put
catalog in nets.
Object fountain "sensor fountain" Scanner,
with
name 'sensor' 'fountain',
description
"It's a common sensor interface -- a spot where data from
meat-side gushes up into the world. This fountain
is fed by the retinal scanner you're inhabiting. If a
meatsider presses his squashy eyeball against it, a torrent
of digitized eyeprint data will flow from the fountain
into the waiting analysis nets.",
before [;
Receive:
if (noun = = catalog)
<<Insert catalog nets>>;
],
has scenery;