Every example program so far has consisted only of
a sequence of routines, each within beginning and end markers [
and ]
. Such routines have no way of communicating with each
other, and therefore of sharing information with each other, except by
calling each other back and forth. This arrangement is not really suited
to a large program whose task may be to simulate something complicated,
such as the world of an adventure game: instead, some central registry
of information is needed, to which all routines can have access.
In the author's game ‘Curses’, centrally-held
information ranges from the current score, held in a single variable
called score
, to Madame Sosostris's tarot pack, which
uses an array of variables representing the cards on the pack, to a
slide-projector held as an “object”: a bundle of variables
and routines encoding the relevant rules of the game, such as that
the whitewashed wall is only lit up when the slide projector is
switched on.
Every Inform source program is a list of constructions, made using commands called “directives”. These are quite different from the statements inside routines, because directives create something at compilation time, whereas statements are only instructions for the interpreter to follow later, when the story file is being played.
In all there are 38 Inform directives, but most of
them are seldom used, or else are just conveniences to help you organise
your source code: for instance Include
means “now
include another whole file of source code here”, and there
are directives for “if I've set some constant at the start of
the code, then don't compile this next bit” and so on. The 10
directives that matter are the ones creating data structures, and
here they are:
[ Array Attribute Class Constant Extend Global Object Property Verb
The directive written [
, meaning
“construct a routine containing the following statements,
up to the next ]
”, was the subject of
§1. The four directives to do
with objects, Attribute
, Class
,
Object
and Property
, will be the subject of
§3. The two directives to do with laying
out grammar, Verb
and Extend
, are intimately
tied up with the needs of adventure games using the Inform library,
and are useless for any other purpose, so these are left until
§30. That leaves just Array
,
Constant
and Global
.
The simplest construction you can make is of
a Constant
. The following program, an unsatisfying
game of chance, shows a typical usage:
Constant MAXIMUM_SCORE = 100; [ Main; print "You have scored ", random(MAXIMUM_SCORE), " points out of ", MAXIMUM_SCORE, ".^"; ];
The maximum score value is used twice in the routine
Main
. The resulting story file is exactly the same as it
would have been if the constant definition were not present, and
MAXIMUM_SCORE
were replaced by 100 in both places
where it occurs. But the advantage of using Constant
is
that it makes it possible to change this value from 100 to, say, 50
with only a single change to the source code, and it makes the source
code more legible.
People often write the names of constants in full
capitals, but this is not compulsory. Another convention is that the
=
sign, which is optional, is often left out if the
value is a piece of text rather than a number. If no value is specified
for a constant, as in the line
Constant BETA_TEST_VERSION;
then the constant is created with value 0.
A constant can be used from anywhere in the source code after the line on which it is declared. Its value cannot be altered.
The variables in §1 were all “local variables”, each owned privately by its own routine, inaccessible to the rest of the program and destroyed as soon as the routine stops. A “global variable” is permanent and its value can be used or altered from every routine.
The directive for declaring a global variable is
Global
. For example:
Global score = 36;
This creates a variable called score
, which
at the start of the program has the value 36. (If no initial value
is given, it starts with the value 0.)
A global variable can be altered or used from anywhere in the source code after the line on which it is declared.
An “array” is an indexed collection of
variables, holding a set of numbers organised into a sequence. To
see why this useful, suppose that a pack of cards is to be simulated.
You could define 52 different variables with Global
, with
names like Ace_of_Hearts
, to hold the position of each
card in the pack: but then it would be very tiresome to write a routine
to shuffle them around.
Instead, you can declare an array:
Array pack_of_cards --> 52;
which creates a stock of 52 variables, called the “entries” of the array, and referred to in the source code as
pack_of_cards-->0 pack_of_cards-->1 ... pack_of_cards-->51
and the point of this is that you can read or alter
the variable for card number i
by calling it
pack_of_cards-->i
.
Here is an example program, in full, for shuffling the pack:
Constant SHUFFLES = 100; Array pack_of_cards --> 52; [ ExchangeTwo x y z; ! Randomly choose two different numbers between 0 and 51: while (x==y) { x = random(52) - 1; y = random(52) - 1; } z = pack_of_cards-->x; pack_of_cards-->x = pack_of_cards-->y; pack_of_cards-->y = z; ]; [ Card n; switch(n%13) { 0: print "Ace"; 1 to 9: print n%13 + 1; 10: print "Jack"; 11: print "Queen"; 12: print "King"; } print " of "; switch(n/13) { 0: print "Hearts"; 1: print "Clubs"; 2: print "Diamonds"; 3: print "Spades"; } ]; [ Main i; ! Create the pack in quot;factory order": for (i=0:i<52:i++) pack_of_cards-->i = i; ! Exchange random pairs of cards for a while: for (i=1:i<=SHUFFLES:i++) ExchangeTwo(); print "The pack has been shuffled into the following order:^"; for (i=0:i<52:i++) print (Card) pack_of_cards-->i, "^"; ];
The cards are represented by numbers in the range
0 (the Ace of Hearts) to 51 (the King of Spades). The pack itself has
52 positions, from position 0 (top) to position 51 (bottom). The
entry pack_of_cards-->i
holds the number of the
card in position i
. A new pack as produced by the factory
would come with Ace of Hearts on top (card 0 in position 0), running
down to the King of Spades on the bottom (card 51 in position 51).
▲
A hundred exchanges is only just enough. Redefining SHUFFLES
as 10,000
takes a lot longer, while redefining it as 10 makes for a highly suspect
result. Here is a more efficient method of shuffling (contributed
by Dylan Thurston), perfectly random in just 51 exchanges.
pack_of_cards-->0 = 0; for (i=1:i<52:i++) { j = random(i+1) - 1; pack_of_cards-->i = pack_of_cards-->j; pack_of_cards-->j = i; }
· · · · ·
In the above example, the array entries are all created containing 0. Instead, you can give a list of constant values. For example,
Array small_primes --> 2 3 5 7 11 13;
is an array with six entries, small_primes-->0
to small_primes-->5
, initially holding 2, 3, 5, 7, 11
and 13.
The third way to create an array gives some text as an initial value, occasionally useful because one popular use for arrays is as “strings of characters” or “text buffers”. For instance:
Array players_name --> "Frank Booth";
is equivalent to the directive:
Array players_name --> 'F' 'r' 'a' 'n' 'k' ' ' 'B' 'o' 'o' 't' 'h';
Literal text like "Frank Booth" is a constant,
not an array, and you can no more alter its lettering than you could
alter the digits of the number 124. The array players_name
is
quite different: its entries can be altered. But this means it cannot
be treated as if it were a string constant, and in particular can't
be printed out with print (string)
. See below for the right
way to do this.
• WARNING
In the pack of cards example, the entries are indexed 0 to 51. It's
therefore impossible for an interpreter to obey the following statement:
pack_of_cards-->52 = 0;
because there is no entry 52. Instead, the following message will be printed when it plays:
[** Programming error: tried to write to -->52 in the array “pack_of_cards”, which has entries 0 up to 51 **]
Such a mistake is sometimes called breaking the bounds of the array.
· · · · ·
The kind of array constructed above is sometimes called a “word array”. This is the most useful kind and many game designers never use the other three varieties at all.
▲
The first alternative is a “byte array”, which is identical
except that its entries can only hold numbers in the range 0 to 255,
and that it uses the notation ->
instead of -->
. This
is only really useful to economise on memory usage in special circumstances,
usually when the entries are known to be characters, because ZSCII
character codes are all between 0 and 255. The “Frank Booth”
array above could safely have been a byte array.
▲
In addition to this, Inform provides arrays which have a little extra
structure: they are created with the 0th entry holding the number
of entries. A word array with this property is called a table
;
a byte array with this property is a string
.
For example, the table
Array continents table 5;
has six entries: continents-->0
, which holds
the number 5, and further entries continents-->1
to continents-->5
. If the program changed
continents-->0
this would not magically change the
number of array entries, or indeed the number of continents.
▲▲
One main reason you might want some arrangement like this is to write
a general routine which can be applied to any array. Here is an example
using string
arrays:
Array password string "danger"; Array phone_number string "0171-930-9000"; ... print "Please give the password ", (PrintStringArray) password, " whenever telephoning Universal Exports at ", (PrintStringArray) phone_number, "."; ... [ PrintStringArray the_array i; for (i=1: i<=the_array->0: i++) print (char) the_array->i; ];
Such routines should be written with care, as the normal checking of array bounds isn't performed when arrays are accessed in this indirect sort of fashion, so any mistake you make may cause trouble elsewhere and be difficult to diagnose.
▲▲ With all data structures (i.e., with objects, strings, routines and arrays) Inform calls by reference, not by value. So, for instance:
[ DamageStringArray the_array i; for (i=1: i<=the_array->0: i++) { if (the_array->i == 'a' or 'e' or 'i' or 'o' or 'u') the_array->i = random('a', 'e', 'i', 'o', 'u'); print (char) the_array->i; } ];
means that the call DamageStringArray(password_string)
will not just print (say) “dungor” but also alter the one
and only copy of password_string
in the story file.
Surprisingly, perhaps, given that Inform is a language for text adventure games, support for reading from the keyboard is fairly limited. A significant difference of approach between Inform and many other systems for interactive fiction is that mechanisms for parsing textual commands don't come built into the language itself. Instead, game designers use a standard Inform parser program which occupies four and a half thousand lines of Inform code.
Reading single key-presses, perhaps with time-limits, or for that matter reading the mouse position and state (in a Version 6 game) requires the use of Inform assembly language: see §42.
A statement called read
does however
exist for reading in a single line of text and storing it into a
byte array:
read text_array 0;
You must already have set text_array->0
to the maximum number of characters you will allow to be read. (If
this is N, then the array must be defined with at least
N + 3 entries, the last of which guards against overruns.)
The number of characters actually read, not counting the carriage
return, will be placed into text_array->1
and
the characters themselves into entries from text_array->2
onwards. For example, if the player typed “GET IN”:
->0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
max | characters | text typed by player, reduced to lower case | |||||
60 | 6 | 'g' |
'e' |
't' |
' ' |
'i' |
'n' |
The following echo chamber demonstrates how to read from this array:
Array text_array -> 63; [ Main c x; for (::) { print "^> "; text_array->0 = 60; read text_array 0; for (x=0:x<text_array->1:x++) { c = text_array->(2+x); print (char) c; if (c == 'o') print "h"; } } ];
· · · · ·
▲
read
can go further than simply reading in the text:
it can work out where the words start and end, and if they are words
registered in the story file's built-in vocabulary, known as the
“dictionary”. To produce all this information, read
needs to be supplied with a second array:
read text_array parse_array;
read
not only stores the text (just
as above) but breaks down the line into a sequence of words, in
which commas and full stops count as separate words in their own right.
(An example is given in Chapter IV,
§30.) In advance of this
parse_array->0
must havebeen set to W, the
maximum number of words you want to parse. Any further text will be
ignored. parse_array
should have at least 4W
+ 2 entries, because parse_array->1
is set to the
actual number of words parsed, and then a four-entry block is written
into the array for each word parsed. Numbering the words as 1, 2, 3,
…, the number of letters in word n is written into
parse_array->(n*4)
, and the position of the start
of the word in text_array
. The dictionary value of
the word, or zero if it isn't recognised, is stored as
parse_array-->(n*2-1)
. The corresponding parsing
array to the previous text array, for the command “GET IN”,
looks like so:
->0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
max | words | first word | second word | ||||||
10 | 2 | 'get' |
2 | 3 | 'in' |
5 | 2 |
In this example both words were recognised. The
word “get” began at position ->2
in
the text array, and was 3 characters long; the word “in”
began at ->5
and was 2 characters long.
The following program reads in text and prints back an analysis:
Array text_array -> 63; Array parse_array -> 42; [ Main w x length position dict; w = 'mary'; w = 'had'; w = 'a//'; w = 'little'; w = 'lamb'; for (::) { print "^> "; text_array->0 = 60; parse_array->0 = 10; read text_array parse_array; for (w=1:w<=parse_array->1:w++) { print "Word ", w, ": "; length = parse_array->(4*w); position = parse_array->(4*w + 1); dict = parse_array-->(w*2-1); for (x=0:x<length:x++) print (char) text_array->(position+x); print " (length ", length, ")"; if (dict) print " equals '", (address) dict, "'^"; else print " is not in the dictionary^"; } } ];
Note that the pointless-looking first line of Main
adds five words to the dictionary. The result is:
>MARY, hello
Word 1: mary (length 4) equals 'mary'
Word 2: , (length 1) is not in the dictionary
Word 3: hello (length 5) is not in the dictionary
▲
What goes into the dictionary? The answer is: any of the words given
in the name of an object (see §3),
any of the verbs and prepositions given in grammar by Verb
and Extend
directives (see §26), and anything
given as a dictionary-word constant. The last is convenient because
it means that code like
if (parse_array -->(n*2-1)) == 'purple';
does what it looks as if it should. When compiling
this line, Inform automatically adds the word “purple”
to the story file's dictionary, so that any read
statement
will recognise it.
• REFERENCES
Evin Robertson's function library "array.h"
provides some simple array-handling utilities.
•L. Ross Raszewski's function
library "istring.h"
offers Inform versions of
the ANSI C string-handling routines, including strcmp()
,
strcpy()
and strcat()
. The further extension
"znsi.h"
allows the printing out of string
arrays with special escape sequences like [B
interpreted
as “bold face.” (See also the same author's
"ictype.h"
.)
•Adam Cadre's function library
"flags.h"
manages an array of boolean values
(that is, values which can only be true
or false
)
so as to use only one-sixteenth as much memory as a conventional array,
though at some cost to speed of access.