[Main]
[Previous]   [Next]

Programming Prolegomena

Many readers may prefer to skip this section altogether and dive straight into the more interesting business of writing a game. But if you are completely new to programming in TADS (or TADS 3) you may appreciate a brief introduction to some of the basic ground rules. This section makes no attempt to give a comprehensive or systematic account of the TADS 3 language, but simply introduces some of the things you will be meeting in this Getting Started Guide.

a. Overview of Basic Concepts

Writing a game in TADS 3 requires two different styles of programming: declarative and procedural. Declarative programming is largely a matter of defining objects and setting their properties (see below). Setting the properties of objects means giving them values; a value may typically be a number, a string (i.e. a piece of text) or another object. Since adv3, the library that comes with TADS 3, is so rich, you can achieve a great deal in TADS 3 with declarative programming alone.

Procedural programming involves writing a sequence of statements. Each statement is an instruction that you want your game to carry out. Statements may typically assign a value to a variable or property, or call a function or method. A variable is a kind of temporary store for a value; a property can act as a more permanent store.

With one or two exceptions we needn't worry about here, statements can appear only in functions and methods; there needs to be some context in which they are executed. Similarly, variables can only be used in functions and methods; all TADS 3 variables are thus local variables (see further below).

A function is a kind of wrapper for a group of related statements you want to be executed together. An individual function is usually designed to carry out one specific task (although it may be a highly complex task involving many individual steps). The process of telling TADS 3 that we want a function to carry out its task is known as calling or invoking the function (the two terms are synonymous).

A method is similar to a function, but is associated with a particular object. A function can be invoked (i.e. called) simply using its name (e.g. the statement foo() will invoke the function named foo), whereas invoking a method generally requires specifying the name of the object to which it belongs as well (e.g. foo.bar() would invoke the bar method of the foo object). The exception is when a method is invoked from another method of the same object.

b. Objects

Broadly speaking, most programming in TADS consists of defining objects (although you may also find yourself defining classes, functions, and one or two other things, but we'll leave those to one side for the moment). An object may be an object in the physical sense of something that appears in your game world, such as a spade, a cottage, or a shopkeeper, but it may also be a more abstract construct designed to do some job or other in your code. Examples of some of abstract objects we shall be encountering include ActorStates that help describe how an actor behaves under particular circumstances, and TopicEntries that define how an actor responds to various questions.

Objects generally belong in some form of containment hierarchy. For physical objects this usually represents the notional containment relationships in your game world. At the top of the hierarchy are the rooms (locations) that make up the map of your world. Each individual room may contain a number of objects, such as tables, chairs, rocks, boxes and the like, as well as actors such as the player character (PC) and non-player characters (NPCs). These in turn may 'contain' further objects (and so on). For example, if there is coin inside one of the boxes, the coin is contained by the box, just as the box is contained by the room. 'Containment' is, however, a slightly more general relation than this example might suggest. For example, if a pen is sitting on the table, then the table is considered to be the pen's container. Anything held (or worn) by an actor is considered to be contained by the actor. So, for example, if the PC picks up one of the rocks, that rock's container changes from the room to the PC. If the PC then puts one of the boxes on the table, the box is now 'contained' by the table instead of directly by the room (although it remains indirectly contained by the room). At this point the coin is contained by the box, but is also 'in' the table and the room. In TADS 3 the immediately container of an object is always specified in its location property.

Containment may also be used to relate abstract objects. For example, menu items may be contained in a menu, or an actor may 'contain' abstract objects such as ActorStates and TopicEntries (these will be explained in due course) as well as physical objects being carried around by the actor.

Typically an object definition begins with the name of an object, followed by a colon, followed by a class list, followed by a list of its properties and methods:

  myObj: Thing
      name = 'boring object'
      changeName
      {
        name = 'even more boring object';
      }
  ;

In this definition name is a property of myObj, changeName is a method and Thing is the class (or superclass or base class) of the object. The functional difference between a property and a method is that properties hold values while methods contain code: a list of one or more statements that do something when the method is invoked. The syntactical difference is that the name of a property is separated from its value by an equals sign (=) while that of a method is not, the statements that make up the method being enclosed in braces { }.

A further point of syntax to note is the use of the semicolon. This is used (a) to terminate the object definition, and (b) to terminate statements. It is not used to terminate property definitions (a very, very easy mistake to make). Although they look very similar, the line name = 'boring object' is a property definition that means "define a name property on myObj and set its initial value to 'boring object'", while the statement within the changeName method, i.e. name = 'even more boring object'; is an assignment statement that means "change the value of the already existing value of the name property to 'even more boring object'."

Note that you could use braces instead of a terminating semicolon to define the extent of the object definition; the foregoing object definition could then have been written:

  myObj: Thing
  {
      name = 'boring object'
      changeName
      {
        name = 'even more boring object';
      }
  }

Which you use is up to you, but this Guide will use the terminating semicolon.

c. Assignment Statements

An assignment statement is probably one of the most common kinds of statement that you will come across in TADS 3 programming. It always takes the form:

  lvalue = expression; 

Where lvalue can be either an object property or a variable (which we'll talk about in just a bit). An expression can be as simple as a constant value or the name of another variable, a function call or method name (assuming the function or method returns a suitable value), or a more complex expression involving a number of the foregoing elements joined together with operators, for example:

  myName = 'my ' + name;

As a statement this would assign the value 'my boring object' to the variable myName (assuming that name started off by holding the value 'boring object'). Note that an expression can also be used as the value of a property (in which case it should be enclosed in parentheses), so that if we made myName a property of myObj, we could definine it thus:

  myObj: Thing
      name = 'boring object'
      changeName
      {
        name = 'even more boring object';
      }
      myName = ('my ' + name)
  ;

This definition would mean that myName contained 'my boring object' until the changeName method was invoked, and would contain 'my even more boring object' afterwards (we'll talk about invoking methods presently). In fact, it is, except for its appearance, exactly the same as writing:

  myName  { return 'my ' + name; }

When it is used with (single-quoted) strings, + is thus a concatenation operator. With numbers it does what you would expect, i.e. add them together, e.g.:

  myNumber = 3 + 4;

Would assign the number 7 to myNumber. All the numbers we'll be dealing with in this Guide will be integers (i.e. whole numbers); TADS 3 does possess a BigNumber class that allows you to work with real numbers (i.e. numbers including a fractional part, such as 3.14159), but most Interactive Fiction can get by quite happily with standard integer arithmetic.

Other common arithmetic operators include -, * and / (subtract, multiply and divide) which do much what you would expect (note that the division is integer division, so that myNumber = 3 / 4 would set myNumber to zero, while myNumber = 10 / 4 would set it to 2). Less obvious but almost just as common and useful are the various shortcut operators that provide a more concise way of coding common operations. There are several of these, but the only ones we need deal with here are += -= ++ and --. It is quite common in programming to want to add or subtract a number from the current value of a variable or property and store the result in the same variable or property, e.g.:

  myNumber = myNumber + 4;
  myNumber = myNumber - 2;

If myNumber started out at 6, then after the first line was executed, myNumber would be changed to 10, and after the second line was executed, it would be changed to 8. This could be written more succinctly as:

  myNumber += 4;
  myNumber -= 2;

This may look a litle strange at first, but it's a highly convenient feature once you get the hang of it. Another one is the use of ++ or -- to increase or reduce a property or variable by one. Thus intead of writing myNumber = myNumber + 1 or even myNumber+=1 one could write simply myNumber++; likewise one could use myNumber-- in place of myNumber = myNumber - 1.

In these examples, myNumber could be either a property or a variable. In TADS 3 programming properties tend to be used for semi-permanent storage of information you need to be available to the whole program, while variables are local in scope and temporary in duration, used, for example, to hold the results of some intermediate calculation (there are some library defined quanties of the form gWhatsit that look like global variables, but these are simply shorthand ways of referring to some commonly used property of a library object). Being local in scope means that the variable is available only to code within the same block (usually the same method or function) as that in which the variable is defined; being temporary in duration means that the variable only retains its value for that particular invocation of the function or method. A variable must be declared with the keyword local in the block in which it appears, and may optionally be initialized in the same statement in which it is initialized, e.g.:

  local x;
  local numberOfCabbageEaters = 12;

d. Referring to Methods and Properties

Variables, and indeed statements, are generally used within object methods and global functions. But how are the functions and methods used in turn? Often the library will expect a method to be defined on an object you create and will invoke (call) it under the appropriate circumstances; moreover, you can often use a method in place of a property when you want to do something more complex than you can do with a property; then, when the library tries to (say) display the value of the name property it may quite happily use the value returned by the name method instead. If you've defined a method myMethod on an object myObj you can invoke it from anywhere in your code by writing the statement:

  myObj.myMethod;
  or  
  myObj.myMethod();
 

Similarly, you can reference the value of the myProperty property of myObj with myObj.myProperty. Note the use of the dot (.) notation here, since you will be using it a lot.

In TADS 2 (or Inform 6), if you wanted to reference myObj.myMethod() or myObj.myProperty from another property or method of myObj you would typically write self.myMethod() or self.myProperty(), where self is a special keyword meaning "the current object". There are still situations where you may need to use the self keyword in TADS 3 but this is no longer one of them; instead, in this situation, you could write simply, myMethod() or myProperty. To make this clearer, we'll give an example:

myObj: Thing
    name = 'boring object'
    changeName
    {
        name = 'very boring object';
    }
    myName = ('my ' + name)
;

myOtherObject: Thing
    name = 'exciting object'
    describeName
    {
        local dName = 'This is an ' + name + ', unlike ';
        myObj.changeName;
        dName += myObj.myName;
        say(dName);
        return dName;
    }
;

In this example, a call to myOtherObj.describeName should result in the display of the message "This is an exciting object, unlike my very boring object"; moreover, if you wrote a statement such as msg = myOtherObj.describeName, not only would "This is an exciting object, unlike my very boring object" be displayed, but the string 'This is an exciting object, unlike my very boring object' would be stored in the variable msg. This comes about because the last statement of describeName tells the method to return a value (in this case the value of the local variable dName), and this value will be treated as the value of the method if it is used in an expression.

e. Functions and Methods

Functions may return values in similar ways. The purpose of using a function is typically to perform an often-used calculation that is not related to any particular object, e.g.:

function salesTax(salesValue, taxPercent)
{
    return (salesValue * taxPercent)/100;
}

The function keyword used here is optional but perhaps makes the code clearer, although it is more usual to omit it in TADS 3 code. Note that in this example, unless we're using the BigNumber class, salesValue and taxPercent must both be integers (e.g. 120 meaning, say, 120 pence or 120 cents, and 15 meaning 15%). More to the point, note that salesValue and taxPercent are the two formal parameters of this function, which means that they're placeholders for whatever values we want to pass to the function when we call it. So, for example, if from somewhere in the program we called taxPennies = salesTax(120, 15); taxPennies would be assigned the value 18. Methods may also take parameters, so for example we could define:

myObj: Thing
   baseName = 'object'
   myName (qualifier)
   {
      return 'my ' + qualifier + ' ' + baseName;
   }
;

Note the use of extra string spaces so that myObj.myName('boring') returns 'my boring object' rather than 'myboringobject'. Note also that we can also define a method (or function) that takes no arguments by using an empty argument list thus: (). So, for example, we could have defined:

myObj: Thing
    name = 'boring object'
    changeName()
    {
        name = 'very boring object';
    }
    myName = ('my ' + name)
;

And it would have meant precisely the same as the earlier definition without the empty () after changeName. Which you use is entirely up to you.


f. Conditions - If Statements

Often one will want to use methods and functions to perform something a bit more complex than we've shown here. One of the basic requirements of any programming language is to be able to test for conditions and act according to the results. For example, we might want myObj to declare itself as either a boring object or exciting object on the basis of a property used as a flag:

myObj : Thing
   name
   {
       if(exciting)
           return 'exciting object';
       else
           return 'boring object';
   }
   exciting = nil
   myName = ('my ' + name)
;

The new construction introduced here means "if the condition in parentheses following the keyword 'if' is true, carry out the statement on the following line, otherwise carry out the statement following the 'else' keyword". TADS 3 defines two special values, true and nil which mean true and false (N.B., it's very easy, especially if you're used to using another language, to type 'false' when you mean 'nil'; TADS 3 uses nil since it has other uses beyond Boolean false). Since the property exciting contains nil myObj.name will return 'boring object'; if exciting were later changed to true (or to any non-zero number), myObj.name would then return 'exciting object'.

The condition in an if statement can be much more elaborate than the name of a property that evaluates to nil or true. For example, suppose that instead of a boolean (nil or true) exciting property we defined a numeric excitement property, with the rule that the object only becomes exciting if its excitement property exceeds 10. We should then have written the test as if(excitement > 10). Alternatively, we might have decided that the object value was only exciting if its excitement value was exactly 123, in which case the condition would be written if(excitement == 123).

Note that this test for equality uses a double equals sign (==), and must be written this way if this is what you mean. It's very easy to write something like if(excitement = 123) by mistake, in which case the compiler will give you a warning, because it almost certainly isn't what you meant.

You may also want to combine tests using the logical operators and, or and not, which in TADS 3 are defined with &&, || and ! respectively. For example if we have defined a boring property on myObj, we might have wanted the exciting test to be:

    if((!boring && excitement > 12) || excitement == 123)

This would mean, if excitement is equal to 123 or if it's greater than 12 and boring is not true. Note the use of grouping parentheses to resolve any potential ambiguities in the order in which these conditions are evaluated.

There is no need to use the else clause at all, if you don't need it. But what happens if you need more than one statement to be executed if something is true, and/or a whole set of statements to be performed otherwise? In this case, we'd use braces {} to group the statements, for example:

if((!boring && excitement > 12) || excitement == 123) { myIndefiniteArticle = 'an'; return 'exciting object'; } else { myIndefiniteArticle = 'a'; return 'boring object'; }

It's quite common to want to assign one value to something if a condition holds, and another otherwise, for example:

if(length > 5) size = 'big'; else size = 'small';

This is kind of thing is so common that TADS 3 provides a short-cut way of doing it. Instead of writing the above, you could write simply:

size = (length > 5) ? 'big' : 'small';

More generally this ternary operator works as follows:

cond ? true-value : false-value

If cond is true this evaluates to true-value, otherwise it evaluates to false-value.


g. The Switch Statement

It is possible to nest if… else… statements to any required depth, so that one could, for example, have the following:

if(excitement == 0) name = 'very boring object'; else if (excitement == 1) name = 'boring object'; else if (excitement == 2) name = 'moderately boring object'; else if (excitement < 5) name = 'vaguely boring object' else if(excitement < 10) name = 'not too boring object'; else name = 'exciting object';

But the trouble with this is that it can quickly become confusing to keep track of which else is meant to match which if (this can be alleviated by using braces to group the code the way you want, though that can lead to messy-looking and verbose code). In some cases this may be the only way to achieve the effect you want, but in this particular case, where we are simply testing the value of a single variable, it is often easier to use a switch statement; in this case the equivalent switch statement would be:

switch(excitement) { case 0: name = 'very boring object'; break; case 1: name = 'boring object'; break; case 2: name = 'moderately boring object'; break; case 3: case 4: name = 'vaguely boring object'; break; case 5: case 6: case 7: case 8: case 9: name = 'not too boring object'; break; default: name = 'exciting object'; }

Note the use of the break; statements to stop the test 'falling through' to other matches. Since we want the test to fall through if excitement is 3, 5, 6, 7 or 8 we do not define a break statement for those cases. So, for example, if excitement is 6 the switch statement will execute the statements for all the cases following case 6 until it encounters a break; this has the desired effect of setting name to 'not too boring object'. The default case defines what happens if none of the preceding cases is matched.

The switch() statement is not restricted to matching numbers, it can also match (single-quoted) strings, objects, lists, Boolean values (true or nil) or enumerators (which we'll meet again below). Again, the case value need not be expressed as a constant of one of these types, so long as it is an expression that evaluates to a constant value of one of these types.


h. Properties Containing Objects and Lists

This brings us to the final introductory point: so far our examples of properties and variables have all been of ones that contain either numbers, strings, or Boolean values (true or nil); but properties and variables can also contain other data types such as objects, lists, enumerators and function pointers, and although we shall be meeting few enumerators and function pointers in what follows, properties containing objects and lists will be rather more common. The concept of a property or variable containing an object is really no more complicated than that of having them refer to strings or numbers. For example if we had two objects, myObj1 and myObj2, we could, say, use the assignment statement obj = myObj2 and then use obj to refer to myObj2. This may seem a bit pointless at first, but it could be useful if we didn't know in advance which object obj was going to be, and we wanted to write general code that could work equally well with a number of objects. To take a trivial example, suppose we wrote the following function:

function showName(obj) {    say(obj.name); }

This definition would then allow us to call showName(myObj1) to display myObj1.name, showName(myObj2) to display myObj2.name and so on. This example is so trivial that it may still seem pointless, but even in a slightly more complex case the value may start to become apparent:

function talkAbout(obj) {     local msg = 'My ';     msg += obj.name;     msg += ' is really very ';     if(obj.excitement < 10)        msg += 'dull.';     else       msg += 'interesting.';     say(msg); }

Perhaps an even more common use of assigning objects to properties is where other objects need to keep track of them. For example, if I have an object (say 'ball') inside another object ('bag'), then the location property of the ball can keep track of where the ball is by being set to the bag object. If the ball is then moved to the tennis court the location property of the ball object could be set to the tennisCourt object to keep track of it.

At first sight, it may seem that doing it the other way round wouldn't work so well, since, say, using bag.contents to keep track of what's in the bag would only allow one object to be in the bag at the time. In fact this is an example of where one would use a list value. A list is basically a list of items (of any of the valid types, including other lists) enclosed in square brackets and separated by commas, e.g.:

bag.contents = [ball, coin, banana, horseshoe]

To find out whether something's in a list one can use its indexOf method; e.g. bag.contents.indexOf(ball) would be 1; bag.contents.indexOf(banana) would be 3, and bag.contents.indexOf(elixirOfLife) would be nil.


i. Nested Objects

The previous section only scratches the surface of TADS 3 lists; to find out more, look up lists in the System Manual that comes with the TADS 3 Author's Kit. We'll conclude with a rather different kind of list to illustrate one last point, the use of nested objects in TADS 3.

Suppose we have a ball that appears to change colour randomly when we look at it. We might define it like this:

ball: Thing 'ball' 'ball'    "When you look at it, it looks <<colour>>. "     colour  { return colourList.getNextValue(); } ; colourList: ShuffledList    valueList = ['red', 'green', 'blue', 'violet', 'white',       'black', 'orange', 'indigo'] ;

The purpose of a ShuffledList is to return one of its values randomly, without repeating a value until it has used them all. It's a bit like shuffling a pack of cards, then taking one in turn until all have been used, then reshuffling the pack and starting again. But in order to function this way a ShuffledList needs to be a separate object, not only with a list of values (its valueList property) but also a method (getNextValue) that returns the next shuffled value. In order to define the varicoloured ball object, therefore, we also need to define a separate colourList object. While this is far from catastrophic, it can be a little inconvenient, since code that helps to define the behaviour of one object is spread into another; the two objects might in time get separated in your code, or the presence of the second object might mess up the containment hierarchy in some way. This is where a nested object could come in handy.

As an intermediary step, note that a property can contain a reference to an object; for example, we could have written:

ball: Thing 'ball' 'ball'    "When you look at it, it looks <<colour>>. "     colour  { return colourList.getNextValue(); }     colourList = colourListObj ; colourListObj: ShuffledList    valueList = ['red', 'green', 'blue', 'violet', 'white',       'black', 'orange', 'indigo'] ;

And this would work just the same (although it appears a little more verbose). The colour method now refers to the colourList property which in turn refers to the colourListObj object. The way we could make this more compact is to turn the colourListObj object into an anonymous object defined directly on the colourList property:

ball: Thing 'ball' 'ball'    "When you look at it, it looks <<colour>>. "     colour  { return colourList.getNextValue(); }     colourList: ShuffledList     {       valueList = ['red', 'green', 'blue', 'violet', 'white',          'black', 'orange', 'indigo']     } ;

Not only is this more concise, but it has the advantage of keeping all the code together in one object. The ShuffledList has now become an anonymous nested object. All nested objects are anonymous, because they have no name: colourList is not the name of the ShuffledList object here, it's the name of a property of ball; the ShuffledList can nevertheless be referred to as ball.colourList, since it is the value of ball's colourList property. Note that while an ordinary object definition may either be terminated with a semicolon or enclosed in braces, the braces ({}) form must always be used with a nested object, as here.

This may seem a bit strange and convoluted at first, but you'll find the use of anonymous nested objects is a powerful and common feature of TADS 3 programming, so it will be as well to become familiar with it.
   


Getting Started in TADS 3
[Main]
[Previous]   [Next]