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. | Objects
|
|
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:
|
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 propery 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:
|
{
name = 'boring object'
changeName
{
name = 'even more boring object.';
}
}
|
|
|
b. | Assignment Statements
|
|
|
lvalue = expression;
|
|
|
myName = 'my ' + name;
|
|
|
name = 'boring object'
changeName
{
name = 'even more boring object.';
}
myName = ('my ' + name)
;
|
|
myName { return 'my ' + name; }
|
|
|
myNumber = 3 + 4;
|
|
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;
|
|
|
myNumber += 4;
|
myNumber -= 2;
|
|
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;
|
|
|
c. | Referring to Methods and Properties
|
|
|
myObj.myMethod;
|
|
or
|
|
myObj.myMethod();
|
|
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:
|
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;
}
;
|
|
|
d. | Functions and Methods
|
|
|
{
return (salesValue * taxPercent)/100;
}
|
|
baseName = 'object'
myName (qualifier)
{
return 'my ' + qualifier + ' ' + baseName;
}
;
|
|
name = 'boring object'
changeName()
{
name = 'very boring object';
}
myName = ('my ' + name)
;
|
|
|
e. | Conditions - If Statements
|
|
|
name
{
if(exciting)
return 'exciting object';
else
return 'boring object';
}
exciting = nil
myName = ('my ' + name)
;
|
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)
|
|
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';
|
}
|
{
myIndefiniteArticle = 'a';
|
return 'boring object';
|
}
|
f. | The Switch Statement
|
|
|
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';
|
|
|
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.
|
|
g. | Properties Containing Objects and Lists
|
|
|
{
say(obj.name);
}
|
|
{
local msg = 'My ';
msg += obj.name;
msg += ' is really very ';
if(obj.excitement<10)
msg += 'dull.';
else
msg += 'interesting.';
say(msg);
}
|
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]
|
|
|
|
h. | Nested Objects
|
|
Suppose we have a ball that appears to change colour randomly when we look at it. We might define it like this:
|
"When you look at it, it looks <<colour>>. "
colour { return colourList.getNextValue(); }
;
colourList: ShuffledList
valueList = ['red', 'green', 'blue', 'violet', 'white',
'black', 'orange', 'indigo']
;
|
As an intermediary step, note that a property can contain a reference to an object; for example, we could have written:
|
"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']
;
|
|
"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]