Chapter 4: More control structures

If-statements

We have seen how execution can be directed into different branches based on arbitrary conditions, with the help of conjunctions and disjunctions. It is certainly possible to code up a simple if-then-else routine using these constructs alone:

(eat $Obj)
You take a large bite, and conclude that it is
{
{ (fruit $Obj) (or) (pastry $Obj) }
sweet
(or)
($Obj = #steak) (player eats meat)
savoury
(or)
inedible
}.
[Copy to clipboard]

But this quickly becomes unreadable as the code grows in complexity. Dialog provides traditional if, then, elseif, else, and endif keywords:

(if) condition (then)
statements
(elseif) other condition (then)
statements
(elseif) other condition (then)
statements
...
(else)
statements
(endif)
[Copy to clipboard]

The body of code between (if) and (then) is called a condition. If the condition succeeds, possibly binding variables in the process, then any choice points created by the condition are discarded, and execution proceeds with the statements in the corresponding then-clause. Should the condition fail, no variables are bound, and Dialog considers the next (elseif) condition, and so on, eventually falling back on the else-clause if none of the conditions were successful. The (elseif) and (else) clauses are optional; if there is no else clause, it is assumed to be empty (i.e. succeeding). The entire if-statement succeeds if and only if the chosen branch succeeds. Note that if the chosen (then) or (else) block creates choice points, those remain in effect. It is only the conditions that are limited to a single solution.

(eat $Obj)
You take a large bite, and conclude that it is
(if) (fruit $Obj) (or) (pastry $Obj) (then)
sweet
(elseif) ($Obj = #steak) (player eats meat) (then)
savoury
(else)
inedible
(endif).
[Copy to clipboard]

There are subtle differences between the if-statement above and the disjunction shown earlier: An if-condition is evaluated at most once, even if it creates choice points. As soon as one of the then-clauses is entered, all the remaining then-clauses and the else-clauses become inaccessible. And, finally, if-statements without else-clauses succeed when none of the conditions are met (i.e. a blank else-clause is assumed).

In the disjunction-based version of the rule, there are several lingering choice points, so if a failure is encountered further down in the rule (or even in the calling rule, if this was a multi-query), then execution might resume somewhere in the middle of this code, printing half-sentences as a result.

When that happens, it is almost always due to a bug elsewhere in the program. So, arguably, the disjunction-based version is correct. But in the spirit of defensive programming, it's generally a good idea to stick to if-statements when writing code with side-effects, such as printing.

Negation

Is there a way to test whether a query fails? We can certainly do it with an if-else construct:

(if) (my little query) (then) (fail) (endif) My little query failed.
[Copy to clipboard]

But Dialog provides a shorthand syntax for this very common operation. By prefixing a query with a tilde character (~, pronounced “not”), the query succeeds if there is no solution, and fails if there is at least one solution. The following code is equivalent to the if-statement above:

~(my little query) My little query failed.
[Copy to clipboard]

It also works for blocks:

~{ (my little query) (my other little query) }
At least one of the little queries failed.
[Copy to clipboard]

which is equivalent to:

(if) (my little query) (my other little query) (then) (fail) (endif)
At least one of the little queries failed.
[Copy to clipboard]

Dialog also allows us to define rules with negated rule heads. When such a rule succeeds, the query fails immediately, and none of the remaining rules are considered. Negated rules could be thought of as having an implicit (just) (fail) at the end.

(fruit #apple)
(fruit #banana)
(fruit #orange)
(fruit #pumpkin)
(sweet #cookie)
~(sweet #pumpkin)  %% Equivalent to: (sweet #pumpkin) (just) (fail)
(sweet $Obj)
(fruit $Obj)
[Copy to clipboard]

Selecting among variations

When writing interactive fiction, it can be nice to be able to add a bit of random variation to the output, or to step through a series of responses to a particular command. Dialog provides this functionality through a mechanism that is respectfully stolen from the Inform 7 programming language.

To select randomly among a number of code branches, use the expression (select) ...alternatives separated by (or)... (at random):

(descr #bouncer)
The bouncer
(select)
eyes you suspiciously
(or)
hums a ditty
(or)
looks at his watch
(at random).
[Copy to clipboard]

Note that (or) just revealed itself to be an overloaded operator: When it occurs immediately inside a select expression, it is used to separate alternatives. When it is used anywhere else, it indicates disjunction.

Select-at-random never picks the same branch twice in succession, to avoid jarring repetitions in the narrative. If a uniform distribution is desired, e.g. for implementing a die, an alternative form is available: (select) ... (purely at random).

To advance predictably through a series of alternatives, and then stick with the last alternative forever, use (select) ... (stopping):

(report)
(select)
This is printed the first time.
(or)
This is printed the second time.
(or)
This is printed ever after.
(stopping)
(line)
(program entry point)
(report)
(report)
(report)
(report)
[Copy to clipboard]

The output of that program is:

This is printed the first time.
This is printed the second time.
This is printed ever after.
This is printed ever after.

A combination of predictability and randomness is offered by the following two forms, where Dialog visits each alternative in turn, and then falls back on the specified random behaviour:

(select) ...alternatives separated by (or)... (then at random)

(select) ...alternatives separated by (or)... (then purely at random)

To advance predictably through a series of alternatives, and then start over from the beginning, use:

(select) ...alternatives separated by (or)... (cycling)

The three remaining variants from Inform 7 are currently not supported by Dialog.

Stoppable environments

Dialog provides a mechanism for non-local returns, similar to exceptions in other programming languages. By prefixing a statement (such as a query or a block) with the keyword (stoppable), that statement will execute in a stoppable environment. If the built-in predicate (stop) is queried from within the statement, at any level of nesting, execution immediately breaks out of the (stoppable) environment. If the statement terminates normally, either by succeeding or failing, execution also resumes after the (stoppable) construct; (stoppable) never fails. Regardless of how the stoppable environment is left, any choice points created while inside it are discarded.

Stoppable environments can be nested, and (stop) only breaks out of the innermost one. A stop outside of any stoppable environment terminates the program.

Here is a convoluted example:

(routine)
this (stop) (or) that
(program entry point)
{ Let's (or) now. (stop) }
(stoppable) {
take
(routine)
another
}
shortcut
(fail)
[Copy to clipboard]

The printed output is:

Let's take this shortcut now.

The standard library uses stoppable environments to allow action-handling predicates to stop further actions from being processed. For instance, TAKE ALL may result in several actions being processed, one at a time. If taking the booby-trapped statuette triggers some dramatic cutscene, the code for that cutscene can invoke (stop) to prevent spending time on taking the other items.

The status bar

The special syntax (status bar $N), followed by a statement, creates a special kind of stoppable environment: While the inner statement executes, all output will be directed into a status bar area, the height of which will be $N rows. If no status bar area exists yet, one will be created.

Text inside the status bar is always rendered in a fixed-pitch font. When entering the status bar environment, Dialog fills the status bar area with reverse space characters, enables reverse video, and positions the cursor in the top left corner.

The following example draws a very simple status bar:

(status bar 1) { Game over }
[Copy to clipboard]

To create more complex status bars, Dialog offers the following two built-in predicates:

(status bar width $Columns)

Unifies $Columns with the current width, in characters, of the status bar.

(cursor to row $Row column $Column)

Moves the cursor to the indicated row and column (with 1, 1 representing the upper left corner).

It is only legal to invoke (cursor to row $ column $) from within a status bar environment. To remove the status bar, use the built-in predicate (clear all). When reducing the size of the status bar area (e.g. drawing a status bar of height 1 after having drawn one of height 2), be aware that some interpreters hide the extraneous lines, while others regard them as being part of the main window.