Actions are data structures that represent the player's intentions. They are constructed by a part of the Dialog standard library called the parser, in response to commands typed by the player. Once an action has been constructed by the parser, it is passed on to other parts of the program to be processed. Actions have much in common with events in other programming languages.
In Dialog, actions are represented by lists of dictionary words and objects. Here is an example of an action:
[give #apple to #eve] |
Actions are thus a kind of stylized player input. The parser might construct the above action if the player types GIVE APPLE TO EVE, GIVE RED FRUIT TO HER, or even OFFER THE LADY EVERYTHING, depending on circumstances.
Verbs and prepositions are represented by dictionary words in the action. For nearly all of the standard actions, there is at least one form of recognized player input that uses the same words in the same order.
There is a subtlety here: Actions are lists of dictionary words and objects, but raw player input, as returned by (get input $), is also represented by a list of dictionary words. Thus, the parser might encounter the raw player input [inventory], and convert it to the action [inventory], which happens to be the exact same Dialog value. But the player input could equally well have been [i] or [take inventory], and the resulting action would still be [inventory].
In Chapter 10, we will see how the parser produces actions in response to player input. For now, we will take the output of that process, i.e. the action data structures, as our starting point.
To get started, let's consider one of the standard actions: [open $]. By default, this action will fail for objects that are out of reach, non-openable, locked, or already open. Let's add a new rule to prevent opening a particular box while its owner is in the same room as the player:
#box | |
(name *) | box |
(openable *) | |
(prevent [open *]) | |
(current room $Room) %% Get the current room. | |
(#pandora is in room $Room) %% Check if Pandora is here with us. | |
You don't dare do that while Pandora is watching. |
But suppose Pandora isn't here, and the box is within reach, closed, and unlocked. Now the open action will go through, and as a result, (#box is open) is set, and a stock message is printed: “You open the box.”
We can change this stock message in one of two ways. The first and most generally applicable technique is to define a perform rule, overriding the default behaviour of the action:
(perform [open #box]) | |
Shooting guilty glances in every direction, you carefully approach | |
the box, peek under its lid, and slam it down again. The box was empty. |
But this also overrides the default side-effect of setting the ($ is open) flag of the object, so with the above code, the box remains closed after the action has been carried out. The second technique allows us to override only the printed message, while retaining the side-effects. All predefined actions with side-effects (there are eighteen of them, and they are known as the core actions) call out to their own individual narration predicates that we can override:
(narrate opening #box) | |
Shooting guilty glances in every direction, you carefully approach | |
the box, and lift its lid. It seems to be empty. |
Sometimes it makes more sense to keep the action processing just the way it is, and tack on new behaviour at the end:
(after [open #box]) | |
(par) | |
An inexplicable sense of dread comes over you. |
Now, let's consider going between rooms. As we learned in the chapter on moving around, the predicate (from $ go $ to $) defines obvious exits. This predicate is consulted by the default rules for movement-related actions, but we can override those rules in order to implement non-obvious exits, to block obvious exits, or to trigger cutscenes. In most situations, the action we want to intercept is [leave $Room $Dir]: the action for leaving a room in a given direction.
#shed | |
(room *) | |
(your *) | |
(name *) | shed |
(look *) | You are in your shed. The exit is east. |
(from * go #east to #outdoors) | |
(from * go #out to #east) | |
(prevent [leave * #east]) | |
But the world is such a wicked place. | |
#chair | |
(name *) | wooden chair |
(on-seat *) | |
(* is #in #shed) | |
(instead of [leave #shed #up]) | |
(try [climb *]) |
In that example, the only obvious exit is to the east, but it doesn't work. Going up, on the other hand, is reinterpreted as a different action: [climb #chair].
Now that we've seen how to override the default behaviour of some of the standard actions, it's time to look under the hood and see the actual machinery that makes this work.
Once the parser has understood a command typed by the player, and encoded it as a series of actions, each action is tried in turn. Trying an action involves several predicates, as illustrated by the following chart:
Everything starts with (try $), which is a predicate provided by the standard library. The parameter is an action, and try makes queries to (refuse $), *(before $), and (instead of $), passing the action along as a parameter. Briefly, the purpose of refuse is to ensure that every object mentioned in the action is within reach of the player character, and the purpose of before is to automatically carry out certain mundane actions for the player, such as opening doors before going through them. Refuse is invoked twice, just to make sure that before didn't mess things up.
(instead of $) is responsible for looking at an action in detail, determining whether its particular prerequisites are met, and actually carrying it out. The default implementation of instead of delegates these responsibilities to (prevent $), (perform $), and *(after $), again queried with the action as parameter. Finally, many of the standard (perform $) rules make queries to action-specific predicates such as (descr $). But at this point, the parameters are usually objects.
Together, refuse, before, instead of, prevent, perform, and after are known as the six action-handling predicates. Stories typically define rules for them in order to extend, adjust, or override the default behaviour of the standard library. Each action-handling predicate can be intercepted to serve a variety of purposes. Before we dive into that, however, it is necessary to introduce two important mechanisms: Stopping the action, and ticking (advancing time).
The parser may generate several actions in response to a single line of player input. These are tried in turn inside a stoppable environment, and therefore every action-handling rule has the power to stop subsequent actions using the (stop) built-in predicate. It is generally a good idea to invoke (stop) when we have reason to believe that the player has been surprised: When actions fail, or when dramatic cutscenes have played out.
After an action has been tried, the standard library will generally advance time in the game world, by querying a predicate called (tick). The default implementation of (tick) makes multi-queries to the story-supplied predicates (on every tick), (on every tick in $Room), (early on every tick), and (late on every tick). These can be used to print flavour text, move non-player characters, implement daemons and timers of various kinds, or anything else the story author might think of.
Time is not advanced after commands, i.e. actions such as [save] and [transcript off] that take place outside the game world.
When an action-handling predicate decides to (stop) everything, this also prevents the usual ticking from being carried out. Therefore, a common design pattern in action handlers is (tick) (stop), which causes time to pass as a result of the present action, but stops any subsequent actions.
Now we return to the six action-handling predicates. We will not consider them in chronological order; instead we will start with instead of, prevent, perform, and after as these are of most interest to story authors.
Let us begin by looking at the catch-all rule definition for (instead of $), as implemented in the standard library. There are more specific rule definitions preceding it in the library, but this is the base case:
(instead of $Action) | |
~{ (prevent $Action) (tick) (stop) } | |
(perform $Action) | |
(exhaust) *(after $Action) |
We see that if prevent succeeds, the action fails (after advancing time). Thus, a story author can easily prevent a particular action from succeeding:
(prevent [eat #apple]) | |
You're not hungry. |
Since the story file appears before the standard library in source-code order, its rules take precedence: There could be other prevent-rules in the library, but they will have no influence on eating the apple.
Here's a variant where the rule is conditioned by a global flag:
(prevent [eat #apple]) | |
~(the player is hungry) | |
You're not hungry. |
If no prevent-rule succeeds, control is passed to the (perform $) predicate. This is where the action is carried out, as per the following example:
(perform [read #welcomesign]) | |
The sign says “WELCOME”. | |
(perform [read #loiteringplaque]) | |
The plaque says “NO LOITERING”. |
There are two important differences between prevent and perform: The first is that the sense of prevent is negated, meaning that the action fails when the predicate succeeds. The second is that (stop) is invoked automatically when a prevent-rule succeeds. Thus, the above example (with a bit of surrounding context) could lead to the following exchange:
But the standard library contains a generic prevent-rule that causes [read $] to fail when the player is in a dark location. Prevent-rules have precedence over perform-rules (this follows from the implementation of (instead of $) that we saw earlier), so if the player attempts the same command in darkness, the process grinds to a halt already after the first failed attempt:
Recall that prevent-rules defined by the story take precedence over prevent-rules defined by the standard library. Sometimes this is not desirable. For instance, consider the following story-supplied rule:
(prevent [eat $]) | |
~(the player is hungry) | |
You're not hungry. |
Now, if the player attempts to eat a kerosene lamp, the game might refuse with a message about the player not being hungry. It would be more natural, in this case, to complain about the object not being edible, which is handled by a rule in the standard library. To get around this problem, we may wish to intercept perform instead of prevent:
(perform [eat $]) | |
~(the player is hungry) | |
You're not hungry. | |
(tick) (stop) %% These are our responsibility now. |
Likewise, a story might contain situations where the prevent-perform dichotomy breaks down, and it doesn't make sense to check for all the unsuccessful cases before moving on to the successful cases. An alternative approach is to combine everything into a large if/elseif-complex in a perform rule. As long as the unsuccessful branches end with (tick) (stop), that's a perfectly valid and useful approach in story code. In library code, having separate prevent and perform stages is preferable, since that structure is easier to adapt and extend from the outside.
After (perform $) succeeds, the library makes a multi-query to (after $). This allows the story author to schedule events, such as cutscenes or reactions from non-player characters, after specific actions. Because of the multi-query, every possible branch of the after rule is exhausted, which means that several such rules can be attached to any given action.
The library never does anything in the (after $) stage—it's reserved for the story author.
Be aware that some actions call out to other actions, using (try $), as part of their default perform rule. For instance, [greet $] will fall back on [talk to $] in this way. As a consequence, the after rules of the inner action (talk to) are carried out before the after rules of the outer action (greet).
We have seen how to override the perform rule of a standard library action, in order to do something else entirely. But what if you wish to retain the default behaviour of an action, such as taking an object, and merely add some flavour to the message that is printed? As we will see in the chapter on Standard actions, the library defines eighteen core actions that are capable of modifying the game world. Each of these actions has a perform rule that calls out to a specific narration predicate, that you can intercept. Thus, for instance, the following saves you the trouble of updating the object tree to reflect the new location of the apple:
(narrate taking #apple) | |
(#apple is pristine) | |
You pluck the ripe fruit from the tree. |
Likewise, some of the standard actions for exploring the game world call out to action-specific predicates, partly to save typing on the part of the story author, and partly to perform extra work before or afterwards:
(perform [examine #box]) | |
It's a small, wooden box. | |
%% This works, but the rule head is cumbersome to type. It also | |
%% inhibits the default behaviour of invoking '(appearance $ $ $)' for | |
%% items inside the box. | |
(descr #box) | |
It's a small, wooden box. | |
%% This gets queried by the default perform-rule for examine. |
Quite often, the action as reported by the parser could be understood as a roundabout way of expressing a different action. Thus, climbing a staircase in a particular location might be a natural way for the player to express a desire to [go #up]. Certainly, it should not be interpreted as a request to place the player character on top of the staircase object. A well-implemented story will handle these cases transparently, by transforming what the player wrote into what the player intended. This is called diverting the action, and it is achieved by intercepting the (instead of $) rule, and querying (try $) with the desired action. This circumvents the normal prevent-checks, which is good: After all, we don't want the standard library to complain about the staircase not being an actor supporter.
(instead of [climb #staircase]) | |
(current room #bottomOfStairs) | |
(try [go #up]) | |
(instead of [climb #staircase]) | |
(current room #topOfStairs) | |
(try [go #down]) |
There is a subtlety here, related to how time is advanced in the game world: The general rule is that code that queries (try $) is responsible for also calling (tick) afterwards. But when we divert to a different action, we're already inside an action handler, so we trust that whatever code queried us, is eventually going to query (tick) as well.
Stories may invoke (try $) directly to inject actions into the gameplay, e.g. as part of a cutscene. This is typically done at the end of a cutscene, followed by (tick) (stop).
Now let's return to the two remaining action-handling predicates: refuse and before. Consider this an advanced topic: Most of the time, story authors won't need to deal with these predicates directly.
To understand how they fit into the picture, we'll first take a look at the rule definition for (try $), as it is given in the standard library:
(try $Action) | |
~{ (refuse $Action) (stop) } | |
(exhaust) *(before $Action) | |
~{ (refuse $Action) (stop) } | |
(instead of $Action) | |
(try $) | |
%% Succeed anyway. |
If refuse succeeds, all subsequent action handling stops. Time is not advanced. The default implementation of refuse checks that all objects mentioned in the action (except directions and relations) are within reach of the current player character. If they're not, refuse prints a message about it and succeeds, just like a prevent rule. The reason for having two different rules (refuse and prevent), is that it's generally a good idea to check for reachability first. The action-specific prevent-rules are then free to phrase their failure messages in a way that presupposes reachability (e.g. “the door is locked”, which you wouldn't know if you couldn't reach it).
Some actions do not require every object to be within reach. The most common way to modify refuse is to add a negated rule definition. So, for instance, examining does not require reachability:
~(refuse [examine $]) %% Don't refuse. |
Another option is to require reachability for one object, but not the other. Here's a snippet from the standard library:
(refuse [throw $Obj at $Target]) | |
(just) | |
{ | |
(when $Obj is not here) | |
(or) (when $Target is not here) | |
(or) (when $Obj is out of reach) | |
} |
The above code makes queries to when-predicates; these check for common error conditions and print appropriate messages. The full set of when-predicates is documented in Chapter 11.
Also note the (just) keyword, which turns off the default refuse-rule that is defined later in the source code.
When a story overrides refuse, the parameter is often bound to a specific object. So, for instance, a rain cloud in the sky might be out of the player character's reach, But RAIN would be understood as referring to the cloud. In order to allow DRINK RAIN, we might want to make an exception:
~(refuse [drink #cloud]) | |
(instead of [drink #cloud]) | |
You catch a raindrop on your tongue. |
Note that we also decided to bypass the normal prevent-checks by intercepting instead of rather than perform. Another option would be to declare the cloud to be (potable $).
Finally, before-rules smoothen gameplay by taking care of certain well-known prerequisite actions. Thus, if the player attempts to go through a closed door, the game will automatically attempt to open it first. And before that, if the door is locked and the player holds the right key, an attempt is made to unlock the door. Try exhausts every branch of the *(before $) multi-query, so there can be several before-rules for any given action.
By convention, before-rules should use (first try $) to launch the prerequisite actions:
(before [drink #bottle]) | |
(#bottle is closed) | |
(first try [open #bottle]) |
(first try $) prints the familiar “(first attempting to ...)” message, before querying (try $), and then (tick). Ticking is important here, because e.g. opening a door and entering the door should consume two units of time, even when the opening action is triggered automatically by the game.
This is an advanced topic. Feel free to skip this section and return to it later.
When the player types something like EAT OYSTER, HAM AND CHEESE, the usual outcome is that three separate actions are tried in sequence, i.e. [eat #oyster], [eat #ham], and [eat #cheese].
It is possible to instruct the library to combine some of these actions into group actions. For instance, we could declare that ham and cheese, in that order, should form a group:
(action [eat $] may group #ham with #cheese) |
The first action, [eat #oyster], is still passed through the usual action-handling predicates, but the remaining two are combined into [eat [#ham #cheese]] which gets handed off to a set of group-action handling predicates:
(group-refuse $GroupAction)
By default, the group action is refused if (refuse $) succeeds for any of the constituent actions, i.e. (refuse [eat #ham]) or (refuse [eat #cheese]).
(group-before $GroupAction)
By default, this predicate invokes *(before $) for each constituent action.
(group-instead of $GroupAction)
By default, this predicate invokes (group-prevent $), (group-perform $), and (group-after $), in the same way that the default rule for (instead of $) invokes (prevent $), (perform $), and (after $).
(group-prevent $GroupAction)
By default, the group action is prevented if (prevent $) succeeds for any of the constituent actions.
(group-perform $GroupAction)
By default, the group action is performed by querying (perform $) for each constituent action in turn. But the story author will typically override this with some code that performs and reports everything in one go.
(group-after $GroupAction)
By default, this predicate invokes *(after $) for each constituent action.
Thus, we might define a rule for eating the ham and cheese in one go:
(group-perform [eat [#ham #cheese]]) | |
You savour the combination of ham and cheese. | |
(now) (#ham is nowhere) | |
(now) (#cheese is nowhere) |
In many ways, the default behaviour of these rules is sensible and non-surprising, but there are two important gotchas:
In the above example, we allowed two specific objects, #ham and #cheese, in that particular order, to form a group. The parser is allowed to rearrange objects to form groups, as long as their internal order is preserved. Thus, EAT HAM, OYSTER AND CHEESE would result in the group action [eat [#ham #cheese]] followed by the normal action [eat #oyster].
It is also possible to allow entire classes of objects to be grouped together. Here we use the (edible $) trait:
(action [eat $] may group (edible $) with (edible $)) |
Assuming the oyster, the ham, and the cheese are all marked as edible, this will cause the input EAT CHEESE, HAM, OYSTER to resolve into the single group action [eat [#cheese #ham #oyster]]. A corresponding group-perform rule could look like this:
(group-perform [eat $List]) | |
You savour the combination of (the $List). | |
(exhaust) { | |
*($Obj is one of $List) | |
(now) ($Obj is nowhere) | |
} |
The predicates (group-try $) and (first group-try $) behave like (try $) and (first try $), but for group actions. Thus, for instance:
(group-instead of [eat [#cheese #ham]]) | |
(group-try [eat [#ham #cheese]]) | |
(group-before [eat [#ham #cheese]]) | |
(first group-try [put #salt #on [#ham #cheese]]) |