Roger Firth's IF pages

Home

Inform 6: Frequently Asked Questions

Email
Back up

Into
the Intro

Setting
the scene

Preparing
to program

Learning
the lingo

Dabbling
in data

Operating
on objects

Verbal
versatility

Bothered
by bugs

History and
hereafter

Worldly
woes

Inside
information

Tips and
techniques

These topics are about constant and variable data:

How do constants and variables differ?
What can be stored in a variable?
    Inform's number range warning
What about fractions and decimals?
How do global and local variables differ?
What does an array provide?
What's an 'unsigned' number?
    Signed and unsigned comparison
What exactly are 'true' and 'false'?
How do I return data values from a routine?
Where do 'random' numbers come from?

How do constants and variables differ?

A variable is a value which can change, possibly many times, while the game is being played; typical examples are location and score. A constant is a fixed value, set when when the game is compiled; it physically cannot change during play. For example, if you write:

  if (score == 100) { ... }

then pretty clearly the value of 'score' (a variable) might change at any time, but '100' (a constant) is never going to be anything other than the number between 99 and 101.

It's actually better programming practice to write this alternative form (which does, however, behave identically):

More information in the DM:
§2.2 §2.3

  Constant MAX_SCORE = 100;
  ...
  if (score == MAX_SCORE) { ... }

There are two reasons for this. When you read through your game and happen upon that second if statement, the word MAX_SCORE makes it fairly apparent what the test is for. Also, if you later decided to change the game's maximum score, it's easier and more reliable to change the value assigned -- in one place -- to MAX_SCORE than it is to search though the program looking for values of '100' which may need to change. Here too you can see the advantage of naming constants in upper case -- it makes their distinction from variables visually apparent.

What can be stored in a variable?

Roughly speaking, the value that you can store in a variable is one of two things:

This difference is very important: arithmetic using numbers is fine, but arithmetic with Inform data types makes no logical sense. However, because Inform can't tell whether what's stored in a variable is a number or the other stuff, it doesn't stop you from performing meaningless operations, like dividing an object by a string. Be careful -- the onus is entirely on you to use your variables sensibly.

More information in the DM:
§1.4

Bear in mind that the range of numbers that Inform handles is quite small. For example, if you multiply 1000 by 1000 in Inform, the result isn't one million: it's 16960, and you don't get any warning that an overflow has occurred.

What about fractions and decimals?

Inform deals only in integers -- whole numbers, without a decimal point. If you type this:

  X = 25 / 4;

then Inform rounds down the answer and sets X to 6. Worse, if you type this:

  X = 6.25;

then Inform looks at object number 6 (usually compass) and sets X to the value of its property number 25 (usually list_together). Basically, the value "six and a quarter" doesn't exist in Inform.

If you really need them, some mathematical extensions are available. Almost always, however, you'll find that you can rethink what you're trying to do so that integer arithmetic is sufficient.

How do global and local variables differ?

More information in the DM:
§1.5 §1.7

You can declare a global variable, capable of holding a single value, with this directive:

  Global globalCount = 42;

A global variable is accessible from anywhere in the program, which is what primarily distinguishes it from a local variable -- one declared within a routine -- which can be used only within that routine:

  [ myRoutine singleCount; ... ];

The other significant point of difference is that, once set, the value stored in a global variable remains the same until you explicitly change it. Local variables, on the other hand, are reset every time that you call the routine, either by an argument supplied as part of the call or to zero. Think of it this way: any routine can define up to seven local variables, which double as the incoming argument values:

  [ myRoutine V1 V2 V3 V4 V5 V6 V7; ... ];

Each time that you call myRoutine() -- without any arguments -- all seven variables are initialized to zero before the routine's code is executed. If instead you call, for example, myRoutine(100,200,300) then V1, V2 and V3 are initialized to those three values, while the remaining four variables are set to zero.

What does an array provide?

More information in the DM:
§2.4

An array behaves like a global variable, but is capable of holding several values rather than just one. So, you could write, for example:

  Array multiCount --> 10;

to give yourself ten global variables multiCount-->0, multiCount-->1 ... multiCount-->9. Now clearly there's no advantage whatsoever in bundling together a pile of unrelated globals into an array -- you're much better off defining separate globals with meaningful names. The power of an array comes when you find yourself in a position to manipulate the appropriate variable using that 'index' number at the end.

There's helpful introduction by Zak McKracken and Sonja Kesserich to creating and initializing arrays

It's rare, however, for newcomers to find themselves in that position. You can safely ignore the use of arrays until you've written a fair bit of Inform code and have built up your confidence in the way that the language works.

Note that Inform has no concept of a local array, one that is accessible only within a routine.

What's an 'unsigned' number?

In an Inform game, each number is stored as a pattern of binary digits (bits), with sixteen bits being grouped together to form a 'word'. Since each bit in a word can have only two states -- 0 and 1 -- the sixteen of them can be combined in 65536 (=2x2x2x2x2x2x2x2x2x2x2x2x2x2x2x2) different patterns. By a widely-accepted convention, those patterns are logically associated with two series of 65536 decimal numbers: signed numbers ranging from -32678 upwards through 0 and on to 32767; and unsigned numbers ranging from 0 up to 65535. Note that the words 'signed' and 'unsigned' refer only to the way that a bit pattern is interpreted as a decimal number, not to the way that it is stored.

Here are some of the patterns, showing how they are associated with signed and unsigned numbers ($$ is Inform's way of introducing a binary value). You'll notice that in the first half of the table -- when the leftmost bit in the pattern of sixteen is 0 -- the signed and unsigned values are the same. In the second half -- when the leftmost bit is 1 -- they differ radically. You won't be surprised to hear that that leftmost bit is commonly known as the 'sign bit'.

More information in the DM:
§1.4

  Binary bit pattern   Signed  Unsigned
                      decimal   decimal
  -------------------------------------
  $$0000000000000000        0         0
  $$0000000000000001        1         1
  $$0000000000000010        2         2
  $$0000000000000011        3         3
  $$0000000000000100        4         4
    ...                   ...       ...
  $$0111111111111100    32764     32764
  $$0111111111111101    32765     32765
  $$0111111111111110    32766     32766
  $$0111111111111111    32767     32767
  -------------------------------------
  $$1000000000000000   -32768     32768
  $$1000000000000001   -32767     32769
  $$1000000000000010   -32766     32770
  $$1000000000000011   -32765     32771
  $$1000000000000100   -32764     32772
    ...                   ...       ...
  $$1111111111111100       -4     65532
  $$1111111111111101       -3     65533
  $$1111111111111110       -2     65534
  $$1111111111111111       -1     65535
  -------------------------------------

Actually, you very rarely need worry about this; Inform treats almost all numbers as signed, so if you take any two values lying in the range -32768..0..32767 and add them, subtract them, multiply them or whatever, you'll automatically get the expected answer (providing, of course, that the answer is also in that range).

There's one -- rare -- situation where the difference between signed and unsigned numbers is important: the conditional operators < and > perform a signed comparison. To explain what this means, imagine that the variable V1 contains $$0000000000000010, the variable V2 contains $$1111111111111100, and that you're testing if (V1 > V2). Now, since > performs a signed compare, V1's value is taken as 2, and V2's value is taken as -4; the test becomes (2 > -4), which correctly evaluates to true. However... if for some obscure reason the values in V1 and V2 had been object addresses, or pointers, or some other unsigned numbers, then the test should have been treated as (2 > 65532), which of course ought to evaluate to false, so you'll get the wrong answer. The moral is: if you need to test unsigned numbers, use UnsignedCompare() rather than < and >.

What exactly are 'true' and 'false'?

More information in the DM:
§1.4 §1.8

There are two answers to this question. Physically, true is a constant with a value of '1', and false is a constant with a value of '0'. Logically, things are slightly different: 'logical' false is still zero, but 'logical' true is any non-zero value, not just 1.

Testing whether myVar is 'true'

Testing whether myVar is 'false'

 

if (myVar) { ... }

if (~~myVar) { ... }

if (myVar == true) { ... }

if (myVar == false) { ... }

if (myVar ~= false) { ... }

if (myVar ~= true) { ... }

if (~~myVar == false) { ... }

if (~~myVar == true) { ... }

if (~~myVar ~= true) { ... }

if (~~myVar ~= false) { ... }

if (~~(myVar == false)) { ... }

if (~~(myVar == true)) { ... }

if (~~(myVar ~= true)) { ... }

if (~~(myVar ~= false)) { ... }

if ((~~myVar) == false) { ... }

if ((~~myVar) == true) { ... }

if ((~~myVar) ~= true) { ... }

if ((~~myVar) ~= false) { ... }

 

This distinction matters when you're using conditional statements like if. Suppose that you wish to test a true/false variable myVar; there are about eighteen ways in which you could construct the if statement. The nine variations on the left are all triggered when myVar contains true (1), and the matching nine on the right are all triggered when myVar contains false (0).

However, suppose that myVar happens to contain 2, or 100, or -1; all values which, being non-zero, are logically true. Six of those eighteen statements don't work as you might have hoped: the three red statements on the left will fail to trigger, and the three on the right will be triggered unexpectedly. If we also discount the other eight shaded statements as being over-complex, that leaves only four 'reliable' ways of constructing the statement.

The bottom line is: test against false rather than against true. If you want to do something when a value is logically true, use either of:

  if (myVar) { ... }
  if (myVar ~= false) { ... }

and the complementary tests, when a value is logically false, should use either of:

  if (~~myVar) { ... }
  if (myVar == false) { ... }

More information in the DM:
Table 1B

It's occasionally worth remembering that all Inform conditions (for example, a >= b and MyObj in location) evaluate to either 1 or 0, values which you can use in an expression. For example, here's a simple routine, similar to UnsignedCompare(), to compare two numbers:

  [ Compare a b;
      if (a > b) return 1;
      if (a < b) return -1;
      return 0;
  ];

and here's the same thing using a little arithmetic cunning:

  [ Compare a b;
      return (a > b) - (a < b);
  ];

How do I return data values from a routine?

More information in the DM:
§1.7 §3.5

Inform has two types of routine: a standalone routine which is specified independently, and an embedded routine which is specified as a property value within an object definition. Every time that you call a routine, of either type, you get back a single return value (which you're free to ignore if you want to).

This standalone routine

Can be called like this

And returns the value

[ myRoutine;
    print "Hello, world!";
];
myRoutine();
x = myRoutine();
if (myRoutine()) { ... }
etc

true. Note the difference between the value returned at the end of a standalone routine like this, and that returned at the end of an embedded routine.

[ myRoutine;
    print "Hello, world!";
    rtrue;
];

true.

[ myRoutine;
    print "Hello, world!";
    rfalse;
];

false.

[ myRoutine;
    print "Hello, world!";
    return;
];

true.

[ myRoutine;
    print "Hello, world!";
    return expression;
];

given by evaluating the expression.

This embedded routine

Can be called like this

And returns the value

Object  myObject
  with  ...
        myProperty [;
            print "Hello, world!";
        ],
        ... ;
myObject.myProperty();
x = myObject.myProperty();
if (myObject.myProperty()) { ... }
etc

false. Note the difference between the value returned at the end of a standalone routine, and that returned at the end of an embedded routine like this.

Object  myObject
  with  ...
        myProperty [;
            print "Hello, world!";
            rtrue;
        ],
        ... ;

true.

Object  myObject
  with  ...
        myProperty [;
            print "Hello, world!";
            rfalse;
        ],
        ... ;

false.

Object  myObject
  with  ...
        myProperty [;
            print "Hello, world!";
            return;
        ],
        ... ;

true.

Object  myObject
  with  ...
        myProperty [;
            print "Hello, world!";
            return expression;
        ],
        ... ;

given by evaluating the expression.

As mentioned above, you can pass up to seven arguments to a routine. Here's a simple routine, and an example of it being called:

  [ myRoutine V1 V2;
      V1 = V1 + V2;
      print V1;
  ];

  ...

  myRoutine(100,200);

The routine adds together the values of its two arguments, stores the result in the first argument, and then prints the result -- 300 in our example. Now consider this second call:

  num1 = 100; num2 = 200;
  myRoutine(num1,num2);

How does this second example differ from the first? It doesn't: the two behave identically. The important thing to note is that, after the call, num1 still contains 100. Although the value of local variable V1 is changed from 100 to 300 within the routine, this change is not reflected back to the variable num1 (in technical terms, Inform uses 'call by value' rather than 'call by reference'). In summary:

Suppose that you need to return more than one value from a routine; are you stuck? As it happens, no you're not; there's a technique which enables you to return multiple values, but it requires you to learn about arrays, so you might want to leave it until you're fairly comfortable with Inform. The trick is to pass the name of an array as an argument; your routine then has full read/write access to all of the entries of the array, so you can set return values into as many of those entries as you need. An example will help explain what this means.

Suppose that you need a routine which takes a single numeric argument 'msm' -- the time as the number of minutes since midnight -- and returns three values: the hour, the minute of the hour, and an am/pm indicator. First, we'll write the routine:

  [ getClockTime msm result;                  ! 'result' argument is an array
      if (msm < 0 || msm > 1439) rfalse;      ! error: 'msm' argument out of range
      result-->0 = msm / 60 % 12;             ! 'hours' value   0..11
      if (result-->0 == 0) result-->0 = 12;   ! 'hours' value   12, 1..11
      result-->1 = msm % 60;                  ! 'minutes' value 0..59
      result-->2 = (msm > 719);               ! false for 'am', true for 'pm'
  ];

then we'll define an array with three entries, and show the routine in use:

  Array clockTime --> 3;

  if (getClockTime(the_time, clockTime)) {
      if (clockTime-->0 < 10) print "0";      ! leading zero for 'hours'
      print clockTime-->0, ":";
      if (clockTime-->1 < 10) print "0";      ! leading zero for 'minutes'
      print clockTime-->1;
      if (clockTime-->2) print " pm"; else print " am";
  }

Using this technique (and a big enough array), you can overcome both the limit on seven arguments to a routine, and the restriction of only a single return value.

Where do 'random' numbers come from?

First, what do we mean by a 'random' number? In Inform terms, it's a positive integer in the range 1..32767, ideally chosen completely by chance. Imagine an enormous roulette wheel, with 32767 numbered pockets rather than the usual 37 or 38. Spin the wheel and see where the ball lands: that's our random number.

Except that, using software, it turns out to be really hard to replicate the true randomness of a perfectly-balanced roulette wheel, where at each spin there's exactly one chance in 32767 of a particular number turning up. Instead, we use a pseudo-random number generator (PRNG), an algorithm that generates a sequence of numbers whose elements are approximately independent of each other. Most simple PRNGs (that is, whose size and complexity is appropriate for a text adventure game) generate each random number by applying some formula to the number that was generated last time. Let's illustrate this with a trivial example; to make things easier to understand, we'll restrict our random numbers to the range 1..10:

  Global prev_rand = 1;

  [ trivial_PRNG;
      prev_rand = prev_rand + 7;
      if (prev_rand > 10) prev_rand = prev_rand - 10;
      return prev_rand;
  ];

Calling trivial_PRNG() repeatedly returns a sequence of numbers -- 8 5 2 9 6 3 10 7 4 1 8 5 2 9 6 3 and so on -- which certainly looks a bit random. Closer inspection reveals a couple of problems: the numbers are alternately even and odd, and the whole sequence starts again every time we reach the number 8. The first problem can be solved by using a more sophisticated formula than our trivial "just add 7", but the second one is an intrinsic limitation of this sort of PRNG (in technical terms, thanks to wikipedia, "Because any PRNG run on a deterministic computer is a deterministic algorithm, its output will inevitably have one property that a true random sequence would not exhibit: guaranteed periodicity."). In practice, the fact that any sequence of pseudo-random numbers is certain, sooner or later, to repeat itself isn't usually a problem; what's much more important is using a PRNG algorithm, better than our trivial_PRNG(), which ensures that there's no obvious pattern (such as even/odd alternation, or lots of low numbers followed by lots of high numbers) to the sequence of generated numbers.

Because a PRNG produces each new number by applying some algorithm to the number that it returned on the previous occasion, it's necessary to establish a starting value before calling the PRNG for the first time: this initial number is called a seed, and we sometimes use the phrase "seeding the generator" to refer to the PRNG initialisation process. Just storing a constant value, as we did with prev_rand = 1;, isn't normally a sensible idea, because that would mean our sequence of random numbers always starting from that same constant value and continuing in a predictable fashion. Instead, programs attempt to seed the generator by picking an initial seed value, for example the current time in milliseconds, which should be different for each run.

More information in the DM:
§1.14

So, based on our rough-and-ready simplification of what 'randomness' means, we can see how it applies within Inform. The function random() -- it's built into the Inform language rather than being defined in the library -- returns a random value, and may be called in two formats:

  random(6);                ! Format 1

  random(10, 20, 30, 40);   ! Format 2

The first format returns 1, 2, 3, 4, 5 or 6: that is, a random positive integer in the range 1 to the specified upper limit. Other examples: random(1) always returns 1, random(2) returns either 1 or 2, and random(32767) returns a value in the range 1..32767 (the largest positive integer).

The second format returns 10, 20, 30 or 40: that is, a random selection from the set of specified values; in this instance, we could equally have used 10*random(4). Other examples: random(1,1,1,2) returns either 1 or 2 (with a distinct bias towards 1), random(2,3,5,7,11,13,17,19,23,29,31,37,41,43,47) returns a small prime number, random(n_obj,s_obj,e_obj,w_obj) returns a compass object, random("circle","triangle","square","diamond","pentagon") returns the (packed) address of a string, and random('red','green','blue') returns the (unpacked) address of a dictionary word. Note, by the way, that this format accepts only constant arguments -- random(myVar1,myVar2,myVar3) isn't allowed -- and additionally is not subject to the normal Inform restriction that a routine is limited to seven arguments. Both of these oddities are side-effects of the compiler transforming what looks like a normal routine call into some customised Z-code construct.

More information in the DM:
§7.1

As stated above, the PRNG is automatically seeded at the start of each game to what is hopefully an unpredictable value, thus ensuring that a game which includes calls to random() will behave differently each time that it's run. Usually that's what the author wants: there's little point in writing a puzzle involving chance events if their outcome is always identical and so can be anticipated by the player. There is an exception to this preference for unpredictability, however, and that's when you're testing a game prior to release. In this situation, it's often useful to REPLAY a script containing a master-list of commands which progress through the entire game, exercising every feature. For the script to be unfazed by 'random' events, you need those events to happen identically on each replay: that is, you need your sequence of random numbers to become predictable. This is easily achieved by seeding the PRNG to a constant value rather than one which changes each time. Inform provides three more random() formats to cover this situation:

  random(-1000);            ! Format 3 (argument's absolute value in range 1000..32767)

  random(-100);             ! Format 4 (argument's absolute value in range 1..999)

  random(0);                ! Format 5

More information in the Z-Machine Standards Document:
§2.4 §15

The third format, which returns zero, seeds the PRNG with a starting value of 1000. Subsequent calls using Formats 1 and 2 then return a predictable sequence of 'random' numbers which are repeatable every time that the game runs.

The fourth format, which also returns zero, seeds the PRNG in such a way that subsequent calls using Formats 1 and 2 then return values based upon the simple rising sequence 1,2,...,99,100,1,2,...,99,100,1.2...; this too is repeatable every time that the game runs.

The fifth format, which again returns zero, re-seeds the PRNG so that subsequent calls using Formats 1 and 2 revert to unpredictable random results.

At this point, we need to highlight a couple of potential availability problems. Format 4, whose behaviour is undocumented in the DM4, is Graham's 'suggestion' hidden in the Remarks at the end of Section 2 of the Z-Machine Standards Document; this algorithm is not honoured by all interpreters, which tend to treat this format as though it was Format 3. Format 5, similarly undocumented in the DM4, is flagged in Section 15 of the Z-Machine Standards Document as being considered illegal by most interpreters, though this is much less true today (the Standard dates from 1997). The important point is: just because the PRNG in the interpreter that you use for testing your game behaves in a certain fashion, you cannot assume that your audience will encounter the same behaviour in the interpreters that they use for playing your game.

And the difference in behavior associated with Format 4 leads to another problem. The library defines the debugging verb RANDOM thus:

  [ PredictableSub i;
      i = random(-100);
      "[Random number generator now predictable.]";
  ];

  Verb meta 'random'
      *                           -> Predictable;

If you test your game using an interpreter like Frotz, which honours Format 4 by generating a simple rising sequence, then you'll get very different behaviour than if you test with an interpreter which doesn't distinguish between Formats 3 and 4, generating a true (albeit predictable) random sequence. The library doesn't provide a standard way of specifying this latter sequence, an omission which you might wish to remedy by added this at the end of your game:

  #Ifdef DEBUG;

  [ RandomUnpredictableSub;
      random(0);
      "[Random number generator now unpredictable.]";
  ];

  [ RandomSerialSub s;
      if (s == 0) s = 100;
      random(-s);
      "[Random number generator now serial.]";
  ];

  [ RandomRepeatableSub s;
      if (s == 0) s = 1000;
      random(-s);
      "[Random number generator now repeatable.]";
  ];

  [ RandomSeedSub;
      if (noun < 0) noun = -noun;
      switch (noun) {
        0:          RandomUnpredictableSub();
        1 to 999:   RandomSerialSub(noun);
        default:    RandomRepeatableSub(noun);
      }
  ];

  Extend 'random' replace
      *                           -> RandomSerial
      * 'serial'/'s//'            -> RandomSerial
      * 'repeatable'/'r//'        -> RandomRepeatable
      * 'unpredictable'/'u//'     -> RandomUnpredictable
      * number                    -> RandomSeed;

  #Endif; ! DEBUG

Finally, here are a couple of ways of asserting some control over your random numbers despite the vagaries of interpreters' PRNGs. Jim Fisher has an article on Randomizing Random, which shows a technique for obtaining a good random seed value. Roger Firth's random.h extension, available from the Archive, provides a complete replacement for the PRNG, one that behaves the same across all interpreters.

Into
the Intro

Setting
the scene

Preparing
to program

Learning
the lingo

Dabbling
in data

Operating
on objects

Verbal
versatility

Bothered
by bugs

History and
hereafter

Worldly
woes

Inside
information

Tips and
techniques