TAMEScript is the language that all TAME modules are written in. Through TAMEScript, the author defines the world, its elements, the viewpoints, the actions the user can take, and how to interact with objects based on user input.
Take, for example, the "small adventure" in the tutorial pages at the beginning of the documentation:
module
{
title = "A Small Adventure";
author = "Matthew Tropiano";
email = "email@somesite.com";
}
action general a_quit named "quit";
action general a_look named "look around", "look", "l";
action transitive a_take named "take", "pick up", "t", "p";
action transitive a_examine named "look at", "examine", "x";
action transitive a_open named "open", "o";
action ditransitive a_use named "use" uses conjunctions "with", "and";
// The KEY!
object o_key named "key", "small key"
{
onAction(a_examine)
{
textln("Looks like a small key.");
}
onAction(a_take)
{
if (!hasObject(player, this))
{
giveObject(player, this);
textln("You take the key.");
}
else
textln("You already have this.");
}
onRoomBrowse()
{
textln("There's a key, here.");
}
onPlayerBrowse()
{
textln("A key.");
}
}
// The DOOR!
object o_door named "door" uses determiners "the"
{
init()
{
locked = true;
}
onAction(a_examine)
{
textln("Looks like a wooden door.");
addObjectName(this, "wooden door");
}
onActionWith(a_use, o_key)
{
if (locked)
{
locked = false;
textln("You unlocked the door!");
}
else
{
textln("The door is already unlocked.");
}
}
onAction(a_take)
{
textln("You can't pick up a door.");
}
onAction(a_open)
{
queue a_use, this;
}
onAction(a_use)
{
if (locked)
textln("The door is locked.");
else
{
textln("You open the door.");
world.win = true;
}
}
onRoomBrowse()
{
textln("There's a wooden door, here.");
}
}
// YOU!
player p_guy
{
function oops()
{
local x = irandom(3);
if (x == 0)
textln("I can't do that.");
else if (x == 1)
textln("I don't understand.");
else
textln("I don't think so.");
}
onUnhandledAction()
{
oops();
}
onMalformedCommand()
{
oops();
}
onUnknownCommand()
{
oops();
}
onIncompleteCommand()
{
oops();
}
}
// The ROOM!
room r_startroom
{
init()
{
giveObject(r_startroom, o_key);
giveObject(r_startroom, o_door);
}
}
// HELLO, WORLD!
world
{
init()
{
win = false;
}
afterSuccessfulCommand()
{
if (win == true)
{
textln("You escaped the room!");
quit;
}
else if (objectCount(player) > 0)
{
textln("You have:");
browse(player);
}
}
start()
{
setRoom(p_guy, r_startroom);
setPlayer(p_guy);
textln("Escape the room!");
browse(room);
}
}
First, the module header is declared, which describes this module using a series of key-value pairs - its title, author, and basic contact information.
Next, the basic actions are declared. These are the means of interaction between the user and the module, declaring the words that are tied to each action, as well as what kind of additional input is expected based on the action.
Then, a series of other elements are declared which define the world and the things in them. Each element is a little different in how they are described or declared, but most of them are similar in structure.
Each of those elements contain blocks, which are entry points for executing logic, and the blocks are made up of statements and functions.
TAMEScript is a free-form language. Its delimiters are symbols, operators, and whitespace, but the amount of whitespace is not significant.
The "Hello World" module on the intro page is written like this...
world
{
start()
{
textln("Hello, world!");
}
}
...but it acts no differently if it were written like this:
world { start
(
) {
textln (
"Hello, world!"
) ; }
}
...or like this:
world{start(){textln("Hello, world!");}}
However, it is a good idea to write it in a manner that is readable to others and yourself!
In this documentation, the examples use the Allman Bracing Style and the identifiers that refer to elements are in a format similar to Hungarian Notation, where the identifier is prefixed with a character denoting its type (a_ for "action", o_ for "object", etc.).
These are not hard-and-fast rules for writing modules - the general rule is, "If it is readable, it works."
// This is a single-line comment.
/*
This is a multi
line comment.
*/
/* You can even write them like this. */
Comments are useful for explaining sections of code or make markers around important parts. Of course, be careful
to not over-use them, like this example:
/*
* This is the World.
*/
world
{
/* This is a start block. */
start()
{
// I'm about to print something.
textln("Hello, comment world!");
// Done printing. Time to quit.
quit;
}
}
In order to get anything done in a TAME module, you'll probably be crafting expressions that change and manipulate values.
Values in TAME are represented in several ways.
Booleans:
true
false
Integers:
5
77
3679
20000
Hexadecimal Integers (integer-typed):
0x0
0x0ff
0x0343
0xc452
Floating-point ("floats"):
3.4
2.0
0.08
3456.1234
Exponented Floating-point (float-typed):
2e4
1.53e-3
Strings:
"apple"
"pear"
"Buzz Aldrin"
And some abstract values (these are technically floating-point, internally):
NaN
Infinity
Literal strings (that is, values that are a set of characters bounded in double-quotes) are a little special - they may contain things called "escape characters" that allow you to put characters into them that you would not ordinarily be able to add via your keyboard. An escape character starts with a backslash (\) followed by a set of characters that define the output character. This is especially handy for adding double-quotes (since that would make TAME's compiler think a string was being started or terminated). The TAMEScript compiler recognizes most C-like escape characters, such as:
Escape sequence | Output |
---|---|
\0 | NULL character (UTF-16 0x0000) |
\b | Backspace |
\t | Tab |
\n | Newline |
\f | Form feed |
\r | Carriage Return |
\/ | Forward slash |
\" | Double quote |
\\ | Backslash |
\' | Single quote |
\uhhhh | UTF-16 character (where hhhh is 4 hex digits) |
\xhh | ASCII character (where hh is 2 hex digits) |
A "raw string" is a string of characters started and finished by a backtick (`) that captures everything between the backticks literally. No escape characters. All whitespace is captured as well, since raw strings are multiline.
`This is a raw string. ¡You can even put éxtéñdèd characters in them!`
`This string contains both
newlines and
the leading tabs on each
line.`
An identifier is a word or set of characters that describe the name of something (elements, variables, functions, etc.). Identifiers are alphanumeric (0-9, A-Z, a-z) plus the underscore character (_), but must NOT start with a number.
It is also important to note that identifiers are also case-insensitive! They also cannot share names with reserved keywords.
x
abcd
DATA
Parameter1
this_is_an_identifier
These are some examples of illegal identifiers:
0point // starts with a number - parses as "bad number"
return // keyword
not-good // two identifiers with a dash delimiter between them
Mr.Person // two identifiers with a dot delimiter between them
Expressions are a means of combining values and operators in order to convey more complex values. These are evaluated at execution time.
The values in expressions can be combined or manipulated using several operators.
Caution: Values of one type can be promoted implicitly to another type during evaluation, i.e. 5 will be promoted to a float (5.0) if combined with one (5 + 6.0, yields 11.0, 3 + "apple" yields "3apple").
Some examples of expressions:
5 + 2
3.0 - 5.5
4 * 2.9
2.0 / 3.0
!5
-3.77
true | false
"apple" + "sauce"
"ice" + 9
4 + 5 * 7.0
Expressions can consist of multiple values and operators, as well as functions that return values:
4 + min(0, 8)
floor(5.7) / 9.0
5 + 6 + 9 * 6
Expressions with more than one operator in them may be affected by operator precedence. Without additional delimiters like parenthesis, expressions are evaluated as though they were normal math.
3 + 4 * 6 // 27. "*" has higher precedence than "+".
3 * 4 + 6 // 18. "*" has higher precedence than "+".
In order to affect ordering, you can use parenthesis to delimit sub-expressions.
3 * 4 + 6 // 18. "*" has higher precedence than "+".
3 * (4 + 6) // 30. "4 + 6" is evaluated first, then "3 * 10".
Expressions are nice, but without any real directives, TAME would just be doing math! Enter statements. A statement is a single bit of code that conveys a single step or task. All statements are terminated by semicolons (;).
The first type of statement is an assignment. Assignments define variables and their new values. A variable is stored at the current context level, and stores only one value.
a = 1 + 2; // assigns "a" the value 3.
b = 7.5 * 2; // assigns "b" the value 15.0.
Variables can be used in expressions. Variables that are in expressions but have not been set to anything are false.
a = 1 + 2; // assigns "a" the value 3.
b = a + 5; // assigns "b" the value 8.
c = x; // variable "x" does not refer to a value - assigns "c" the value false.
Variables are also case-insensitive.
val = 1 + 2; // assigns ""val" the value 3.
b = VAL + 5; // assigns "b" the value 8 ("val" and "VAL" resolve to the same value).
B = 9; // assigns "B" the value 9 (overwrites "b" - "b" and "B" resolve to the same value).
By default, all variables are stored on the current context. If you want to assign a value to a variable that is not stored on the context, declare it local. Each assignment of that variable is still not stored.
local x = 5; // assigns "x" the value 5, but not persisted in the context.
textln(x); // adds a "text" cue: "5\n"
x = 3;
textln(x); // adds a "text" cue: "3\n"
Local declarations don't need an assignment - just declaring it initializes it to false.
local x; // assigns "x" the value false, but not persisted in the context.
textln(x); // adds a "text" cue: "false\n"
x = 3;
textln(x); // adds a "text" cue: "3\n"
You can remove a variable's assignment entirely using the clear keyword - the next use of that variable will be treated as though nothing was stored in it, or that it didn't even exist!
x = 5; // assigns "x" the value 5.
textln(x); // adds a "text" cue: "5\n"
clear x;
textln(x); // adds a "text" cue: "false\n"
It is possible to mix local and variables persisted on the context with the same name, but this is not advisable. TAME gives local variable values precedence, but will fall back to the persisted value if it is cleared.
x = 5; // assigns "x" the value 5.
local x = 7; // assigns "x" the value 7.
textln(x); // adds a "text" cue: "7\n"
clear x; // clear local x
textln(x); // adds a "text" cue: "5\n"
clear x; // clear x (persisted)
textln(x); // adds a "text" cue: "false\n"
Variables can be referred to on other elements, and can be read or written to in the same way. To reference a variable on another element, you specify the identifier of the element, then a dot (.), then the variable name.
x // this context's x
o_ball.x // o_ball's x
Functions can also be invoked in a statement, including functions that return something. To invoke a function, you type the name of the function plus a list of parameters to pass to it enclosed in parenthesis, each parameter separated by a comma.
fix(5.6, 0); // rounds 5.6 to the nearest whole number and returns it, but does nothing with it.
textln("Hello, world!"); // adds a "text" cue to the response with "Hello, world!\n" as its content.
floor(5.6); // Calculates the floor of 5.6 and returns it, but does nothing with it.
Each of the parameters in a function (that accepts values) are expressions. They can be either single values or complex expressions that evaluate to a value.
a = 5; // assigns "a" the value 5.
textln("The value of \"a\" is " + a); // adds a "text" cue: "The value of \"a\" is 5".
b = floor(10.5 * 6.7) / 3.0; // calculate a bunch of things and store it in "b".
TAME has a list data structure for storing a contiguous set of values. A new list is declared by putting a list of comma-delimited values or expressions in between square brackets ([ ]).
a = [1, 2, 3, 4, 5]; // assigns a new list of 5 values to "a".
b = []; // assigns a new empty list to "b".
Lists in TAME can contain different types of values in one list, including other lists!
a = [1, "two", 3.0, false, [1, 2, 3]]; // assigns a new list of 5 values to "a".
Getting the value in a list entails writing the identifier assigned to a list, then the index of the desired value in brackets (zero-based - 0 is the first index). If the identifier does not refer to a list, or the index is not a valid index, false is returned. The index is read as an integer, so the index can be an expression that results in a value.
a = [100, 200, 300, 400, 500]; // assigns a new list of 5 values to "a".
b = a[0]; // assigns 100 to "b".
c = a[3]; // assigns 300 to "c".
d = a[9]; // assigns false to "d".
Setting the value in a list entails assigning that identifier and bracketed index to a value, like any other variable. If the identifier is not a list, nothing happens.
a = [0, 0, 0, 0, 0]; // assigns a new list of 5 values to "a".
a[0] = 3; // list is now [3, 0, 0, 0, 0]
a[2] = 5; // list is now [3, 0, 5, 0, 0]
a[a[0]] = 4; // list is now [3, 0, 5, 4, 0]
a[-1] = 5; // list is now [3, 0, 5, 4, 0] (not changed)
a[1] = ["apple", "banana"]; // list is now [3, ["apple", "banana"], 5, 4, 0]
There are also a lot of functions associated with list manipulation.
Since lists can contain other lists, they can be accessed using a second index after resolving the first (this can be many lists deep beyond just two).
a = [[1, 2], [9, 10], ["orange", "pear", "peach"]], 16, 17]; // assigns a new list of 5 values to "a".
b = a[0] // assigns "b" a reference to the list [1,2]
c = a[1][0]; // assigns 9 to "b"
d = a[3][0]; // a[3] is not a list - assigns false to d
Lists are a little special. Whereas most values are treated as single, quantifiable values that can be converted and changed to other types, a list is a collection of values, and are maintained by a reference. This means that when a list is assigned to another variable, the reference to that list is assigned. The list's values are not copied, like what happens to a regular value.
a = [1, 2, 3, 4, 5]; // creates a new list with values, and assigns its reference to "a".
b = a; // assign the reference to "a" to "b"
In the above example, both a and b point to the exact same list. If the contents of the list changes, it'll be reflected in both variables.
a = [1, 2, 3, 4, 5]; // creates a new list with values, and assigns its reference to "a".
b = a; // assign the reference to "a" to "b"
b[0] = 7; // index 0 of the list referenced by "b" is now 7.
c = a[0]; // assign 7 to "c".
Equality among lists is not deep. If you test if one list is equal to another, it compares the reference, not the values inside.
a = [1, 2, 3, 4, 5]; // creates a new list with values, and assigns its reference to "a".
b = a; // assign the reference to "a" to "b"
c = [1, 2, 3, 4, 5]; // creates a new list with values, and assigns its reference to "c".
a == b // this is true
a == c // this is false (different reference)
You will need to write a function if you need to compare the contents of two lists.
A series of statements are encapsulated in blocks. Blocks define sections of significant code, usually defining the conditions for when they get executed in TAME.
For example, in the following module, there are a series of blocks that get called in different situations. Read up on all of the available blocks to get an understanding of how and when they can be called!
action general a_quit named "quit";
action transitive a_examine named "examine", "look at";
// Define a "ball" object.
object o_ball named "ball"
{
// init() blocks are called on startup.
init()
{
// no code in this block.
}
// onAction() blocks on objects are called when objects are
// the target of a specific transitive action.
onAction(a_examine)
{
textln("Looks like a ball!");
}
}
world
{
// init() blocks are called on startup.
// Adds the ball to the world.
init()
{
giveObject(world, o_ball);
}
// The world's start() block is called after all init() blocks are called.
start()
{
textln("Type \"quit\" to quit.");
textln("Type \"examine ball\" to examine the ball.");
}
// onAction() blocks on the world are called when specific general
// actions are performed.
onAction(a_quit)
{
quit;
}
}
Blocks are called due to events or actions initiated by a user - you cannot invoke them directly.
You can explicitly see how and when blocks get called in TAME if you run the module with tracing enabled (but this is just for debugging - tracing adds lots of cues to the response!).
Function blocks consist of a series of statements executed in order, and return a value at the end to the caller. They are referenced by name, and called the same way the built-in, predefined functions are.
The basic anatomy of a function is as follows:
function name (parameter1, parameter2, ...)
{
// ... code goes here ...
return 1; // optional return
}
All functions start with the function keyword, then an identifier for the name, then a comma-separated series of identifiers representing the values that were passed to it to be used in the body of the function.
All parameters in a function are considered local in the function body.
The return keyword terminates a function and returns the value of the expression after it.
function one()
{
return 1;
}
function double(x)
{
return x * 2;
}
function diceRoll(n)
{
local i = irandom(n) + 1;
return i;
}
function printThings(a, b, c)
{
textln(a);
textln(b);
textln(c);
}
Functions defined in TAMEScript always return a value, so if the end of a function is reached without hitting a return, false is returned.
world
{
// This function doubles a value.
function double(x)
{
return x * 2;
}
// This function quadruples a value.
function quadruple(x)
{
// functions can call other functions!
return double(x) + double(x);
}
start()
{
local a = 5;
textln("a is " + a);
a = double(a);
textln("a is " + a);
// function names are case-insensitive
a = DoUbLe(a);
textln("a is " + a);
a = quadruple(a);
textln("a is " + a);
quit;
}
}
Functions can be called from the element they are declared on, or from another element, but variables that are not declared local will be set on the context that the function was called from, except if they belong to another element - it will then be set from the aspect of the originating element of the call.
To call another element's function, you specify the identifier of the element, then a dot (.), then the function call.
container c_test
{
function setY(value)
{
textln("Setting \"y\" on "+identity(this)+ " to " + value);
y = value;
}
}
world
{
function setX(value)
{
textln("Setting \"x\" on "+identity(this)+ " to " + value);
x = value;
}
start()
{
setX(5);
c_test.setY(10);
// values that do not resolve are "false"!
// "world" is the main identifier for the world element.
textln("x is " + x);
textln("world.x is " + world.x);
textln("c_test.x is " + c_test.x);
textln("y is " + y);
textln("world.y is " + world.y);
textln("c_test.y is " + c_test.y);
quit;
}
}
TAMEScript also supports recursive functions. A recursive function is a function that calls itself. This could be handy for mathematical functions that define their values using themselves, or other such scenarios that would be useful.
Be warned though - TAME does not allow you to recurse too deeply in order to keep memory use down. By default, this is a depth of 256, but this can be overridden in module headers.
action open a_fact named "factorial" uses local number;
action general a_quit named "quit";
world
{
// This function returns the factorial of a number.
function factorial(x)
{
if (x <= 1)
return x;
else
return x * factorial(x - 1);
}
// "number" is the local variable
onAction(a_fact)
{
local n = asInt(number);
textln(n + "! = " + factorial(n));
}
onAction(a_quit)
{
quit;
}
start()
{
textln("Type \"factorial\" and a number to compute the factorial of it.");
textln("Don't use too high of a number - a fatal error may occur, or values above 22 will cause integer overflows!");
}
}
A control statement is a type of statement that affects the flow of script execution, which is a fancy way of saying if/else branches, loops, or breaks and interrupts.
An if statement represents a branch in logic - if the expression in the parenthesis evaluates to true or something true-equivalent, the subsequent block (delimited by curly braces) is executed.
if (x == 4)
{
textln("x is 4!");
}
If an if statement is followed by an else block, that block will be executed instead, if the expression is NOT true.
if (x == 4)
{
textln("x is 4!");
}
else
{
textln("x is not 4!");
}
A block of after an "if" or "else" can have the braces omitted if it is just one statement.
if (x == 4)
textln("x is 4!");
else
textln("x is not 4!");
For this reason, you can chain together if-elses...
if (x == 4)
{
textln("x is 4!");
}
else if (x == 5)
{
textln("x is 5!");
}
else
{
textln("x is neither 4 nor 5!");
}
...since this is equivalent:
if (x == 4)
{
textln("x is 4!");
}
else
{
if (x == 5)
{
textln("x is 5!");
}
else
{
textln("x is neither 4 nor 5!");
}
}
A while loop executes a following block of statements as long as the expression in the parenthesis is still true or true-equivalent when the block completes.
local x = 5;
// This code will loop 5 times.
while (x > 0)
{
textln("x is " + x);
x = x - 1;
}
A for loop executes the following block of statements as long as the conditional expression is still true or true-equivalent when the block completes.
For loops have an initializer, conditional, and step part enclosed in parenthesis that drives the loop. Each of these parts are separated by a semicolon (;), except for the last part.
// This code will loop 10 times.
for (x = 0; x < 10; x = x + 1)
{
textln("x is " + x);
}
Each part can be left out of a for loop, except for the conditional. The previous loop can be rewritten as:
x = 0;
for (; x < 10;)
{
textln("x is " + x);
x = x + 1;
}
NOTE: As with any looping construct, there is always the risk of writing a loop that will never finish, creating an infinite loop. TAME has a soft safeguard against this, where if it detects that a certain threshold of operations are executed, it will terminate with a FATAL error (default threshold is 100000 operations). This can be overridden in module headers.
Control keywords, like quit, are not function statements. They do not have parameters. You've seen it all throughout the examples in the documentation - quit stops processing of the module and adds a "quit" cue to the response, telling the shell to stop accepting input and stopping user interaction (and clearing the module context state as well).
There are no limits or rules as to where quit can be called, but to save you plenty of headaches, you should probably use it in one spot, which is when a user wants to end the module.
world
{
start()
{
textln("Doing stuff.");
textln("Quitting...");
quit;
// Nothing after quit gets called - execution stops after it!
textln("Doing more stuff.");
}
}
The queue keyword queues a command to be processed after the current command finishes. All queued commands are processed in the order that they are queued.
All types of actions can be used for creating a queued command, but they must have the correct amount and type of elements for each action.
action general a_test named "test";
action general a_wait named "wait";
action general a_quit named "quit";
/*
Name not required for invoking through other commands -
Unnamed actions are just not callable via user input.
*/
action general a_incr;
action general a_printer;
world
{
init()
{
count = 0;
}
onAction(a_quit)
{
textln("Quitting. Note that the lifecycle blocks do not get called.");
quit;
}
onAction(a_test)
{
textln("Called a_test.");
textln("Enqueueing a_incr.");
queue a_incr;
}
onAction(a_wait)
{
textln("Waiting.");
}
onAction(a_incr)
{
textln("Called a_incr.");
count = count + 1;
textln("Counter is now " + count + ".");
}
onAction(a_printer)
{
textln("Called a_printer.");
}
start()
{
textln("Type \"test\" to test incrementer.");
textln("Type \"wait\" to do nothing.");
textln("Type \"quit\" to quit.");
}
afterSuccessfulCommand()
{
textln("First enqueued actions done. Handling after request.");
textln("Enqueuing a_printer " + count + " time(s).");
local x = 0;
while (x < count)
{
queue a_printer;
textln((x+1) + " queued!");
x = x + 1;
}
}
}
A queued action uses the current state of the context when it executes, not when it is queued.
action modal a_move named "move", "go" uses modes "north", "south", "west", "east";
action general a_quit named "quit";
/*
Name not required for invoking through other commands -
Unnamed actions are just not callable via user input.
*/
action general a_look;
// Declare rooms here because we'll need to reference them immediately in r_center.
room r_south;
room r_north;
room r_east;
room r_west;
room r_center
{
onAction(a_look)
{
textln("Looks like I'm in the center room.");
}
onModalAction(a_move, "north")
{
setRoom(player, r_north);
}
onModalAction(a_move, "south")
{
setRoom(player, r_south);
}
onModalAction(a_move, "west")
{
setRoom(player, r_west);
}
onModalAction(a_move, "east")
{
setRoom(player, r_east);
}
}
// Filling out the rooms here.
extend room r_south
{
onAction(a_look)
{
textln("Looks like I'm in the south room.");
}
onModalAction(a_move, "north")
{
setRoom(player, r_center);
}
}
extend room r_north
{
onAction(a_look)
{
textln("Looks like I'm in the north room.");
}
onModalAction(a_move, "south")
{
setRoom(player, r_center);
}
}
extend room r_east
{
onAction(a_look)
{
textln("Looks like I'm in the east room.");
}
onModalAction(a_move, "west")
{
setRoom(player, r_center);
}
}
extend room r_west
{
onAction(a_look)
{
textln("Looks like I'm in the west room.");
}
onModalAction(a_move, "east")
{
setRoom(player, r_center);
}
}
// Our intrepid... hero?
player p_guy
{
onUnhandledAction(a_move)
{
textln("Can't go that way.");
}
onMalformedCommand(a_move)
{
textln("That is not a valid direction to travel! (north, south, east, west)");
}
}
world
{
onAction(a_quit)
{
textln("Quitting. Note that the lifecycle blocks do not get called.");
quit;
}
start()
{
// init player.
setPlayer(p_guy);
setRoom(p_guy, r_center);
// Kick us off. afterRequest() is not called after start().
queue a_look;
textln("Type \"move\" or \"go\" and a compass direction.");
textln("Type \"quit\" to quit.");
}
afterSuccessfulCommand()
{
// Queued actions do not "remember" the context in which they were called (this is pretty useful, though).
queue a_look;
}
}
Commands queued this way that require objects, like transitive or ditransitive actions, do not require those objects to be accessible.
action modal a_move named "move", "go" uses modes "north", "south", "west", "east";
action general a_look named "look", "look around", "look at room";
action general a_quit named "quit";
action transitive a_examine named "examine", "look at";
action transitive a_use named "use";
// The ORB!
object o_orb named "orb"
{
init()
{
// Pick a room.
roomid = irandom(4) + 1;
}
function isCorrectRoom()
{
return room.id == roomid;
}
onAction(a_examine)
{
if (isCorrectRoom())
textln("The orb is starting to glow!");
else
textln("The orb is doing nothing.");
}
onAction(a_use)
{
if (isCorrectRoom())
{
textln("You teleport out of the adventure!");
quit;
}
else
textln("Nothing happens. Must not be the correct room.");
}
}
// Declare rooms here because we'll need to reference them immediately in r_center.
room r_south;
room r_north;
room r_east;
room r_west;
room r_center
{
init()
{
id = 0;
}
onAction(a_look)
{
textln("Looks like I'm in the center room.");
}
onModalAction(a_move, "north")
{
setRoom(player, r_north);
}
onModalAction(a_move, "south")
{
setRoom(player, r_south);
}
onModalAction(a_move, "west")
{
setRoom(player, r_west);
}
onModalAction(a_move, "east")
{
setRoom(player, r_east);
}
}
// Filling out the rooms here.
extend room r_south
{
init()
{
id = 1;
}
onAction(a_look)
{
textln("Looks like I'm in the south room.");
}
onModalAction(a_move, "north")
{
setRoom(player, r_center);
}
}
extend room r_north
{
init()
{
id = 2;
}
onAction(a_look)
{
textln("Looks like I'm in the north room.");
}
onModalAction(a_move, "south")
{
setRoom(player, r_center);
}
}
extend room r_east
{
init()
{
id = 3;
}
onAction(a_look)
{
textln("Looks like I'm in the east room.");
}
onModalAction(a_move, "west")
{
setRoom(player, r_center);
}
}
extend room r_west
{
init()
{
id = 4;
}
onAction(a_look)
{
textln("Looks like I'm in the west room.");
}
onModalAction(a_move, "east")
{
setRoom(player, r_center);
}
}
// Our intrepid... hero?
player p_guy
{
onUnhandledAction()
{
textln("Can't do that.");
}
onMalformedCommand(a_move)
{
textln("That is not a valid direction to travel! (north, south, east, west)");
}
onMalformedCommand()
{
textln("Can't do that.");
}
onIncompleteCommand(a_move)
{
textln("Need a direction to go.");
}
onIncompleteCommand()
{
textln("You should probably complete that thought.");
}
}
world
{
onAction(a_quit)
{
textln("Quitting.");
quit;
}
start()
{
// init player.
setPlayer(p_guy);
setRoom(p_guy, r_center);
giveObject(player, o_orb);
// Kick us off. afterSuccessfulCommand() is not called after start().
queue a_look;
queue a_examine, o_orb;
textln("The ORB that you are carrying reacts to a certain room.");
textln("Using the ORB in this state will help you escape.");
textln("");
textln("Type \"move\" or \"go\" and a compass direction.");
textln("Type \"quit\" to quit.");
}
afterSuccessfulCommand()
{
queue a_look;
queue a_examine, o_orb;
}
}
You can also queue open actions.
action modal a_switch named "switch language to" uses modes "english", "french", "spanish";
action general a_saysomething named "say something";
action general a_quit named "quit";
/*
Name not required for invoking through other commands -
Unnamed actions are just not callable via user input.
*/
action open a_say uses local "stuff";
world
{
init()
{
language = "english";
}
onAction(a_quit)
{
quit;
}
onMalformedCommand(a_switch)
{
textln("Not a valid language choice.");
}
onModalAction(a_switch, "english")
{
language = "english";
queue a_saysomething;
}
onModalAction(a_switch, "french")
{
language = "french";
queue a_saysomething;
}
onModalAction(a_switch, "spanish")
{
language = "spanish";
queue a_saysomething;
}
onAction(a_saysomething)
{
local x = irandom(3);
if (language == "english")
{
if (x == 0)
queue a_say, "Hello.";
else if (x == 1)
queue a_say, "Cheese.";
else if (x == 2)
queue a_say, "Goodbye!";
}
else if (language == "french")
{
if (x == 0)
queue a_say, "Bonjour.";
else if (x == 1)
queue a_say, "Fromage.";
else if (x == 2)
queue a_say, "Au revoir!";
}
else if (language == "spanish")
{
if (x == 0)
queue a_say, "Hola.";
else if (x == 1)
queue a_say, "Queso.";
else if (x == 2)
queue a_say, "�Adi�s!";
}
}
onAction(a_say)
{
textln("You said, \""+stuff+"\"");
}
start()
{
textln("Type \"say something\", or type \"switch language to\" and \"english\", \"french\", or \"spanish\".");
textln("Type \"quit\" to quit.");
}
}
You can also queue several commands using a transitive or ditransitive action (as a transitive action) with an object container, which will queue an action for each object in the object container element (world, player, room, or container).
action general a_quit;
action transitive a_mash;
action transitive a_shred;
object archetype o_food
{
function getName()
{
return "(FOOD)";
}
onAction(a_mash)
{
textln("Mashed " + getName());
}
onAction(a_shred)
{
textln("Shredded " + getName());
}
onContainerBrowse()
{
textln(getName());
}
}
object o_apple : o_food tagged "food", "fruit"
{
override function getName()
{
return "apple";
}
}
object o_banana : o_food tagged "food", "fruit"
{
override function getName()
{
return "banana";
}
}
object o_cucumber : o_food tagged "food", "vegetable"
{
override function getName()
{
return "cucumber";
}
}
object o_eggplant : o_food tagged "food", "vegetable"
{
override function getName()
{
return "eggplant";
}
}
container c_basket;
world
{
start()
{
giveobject(c_basket, o_apple);
giveobject(c_basket, o_banana);
giveobject(c_basket, o_cucumber);
giveobject(c_basket, o_eggplant);
textln("In the basket, I have:");
browse(c_basket);
textln("The fruits are:");
browseTagged(c_basket, "fruit");
textln("The vegetables are:");
browseTagged(c_basket, "vegetable");
textln("Queueing food to mash...");
queue a_mash : c_basket;
textln("Queueing fruits to shred...");
queue a_shred : c_basket, "fruit";
textln("Queueing quit...");
queue a_quit;
}
onAction(a_quit)
{
quit;
}
}
Queued commands are processed after each lifecycle step in TAME, until no more queued commands need to be processed. See the main TAME diagrams for how this fits into the general flow.
All possible syntaxes of queue are as follows:
queue a_generalaction;
queue a_modalaction, "mode";
queue a_openaction, "opentarget";
queue a_transitiveaction, o_object;
queue a_transitiveaction : c_container;
queue a_transitiveaction : c_container, "tagname";
queue a_ditransitiveaction, o_object;
queue a_ditransitiveaction : c_container;
queue a_ditransitiveaction : c_container, "tagname";
queue a_ditransitiveaction, o_object, o_object2;
The control keyword finish ends the currently-executing action (but continues processing queued actions). Like quit, there are no limits or rules as to where it can be used.
action general a_test named "test";
action general a_anotheraction;
world
{
onAction(a_test)
{
textln("Queueing another action...");
queue a_anotheraction;
textln("Calling finish...");
finish;
// Nothing after finish gets called - execution of the current action stops after it!
textln("Doing more stuff.");
}
onAction(a_anotheraction)
{
textln("Doing another action.");
}
start()
{
textln("Type \"test\" to run the queued action example.");
textln("Calling finish...");
finish;
// Nothing after finish gets called - execution of the current action stops after it!
textln("Doing more stuff.");
}
}
The control keyword end ends the current major entry block (not smaller blocks in control statements). Cannot be used to end function calls - use return for that.
action general a_test named "test";
world
{
onAction(a_test)
{
local i = 0;
for (i = 0; i < 10; i = i + 1)
{
textln(i);
// 1-in-4 chance to stop.
if (irandom(4) == 0)
end;
}
textln("Got to 10!");
}
start()
{
textln("Type \"test\" to run the END example.");
}
}
The break keyword ends the current looping block (while and for). This does not "restore" any variable values or states - it just escapes the loop.
action general a_test named "test";
world
{
onAction(a_test)
{
local i;
local n = irandom(10);
textln("Loop will stop at " + n);
for (i = 0; i < 10; i = i + 1)
{
if (i === n)
break;
textln(i);
}
textln("i is " + i);
}
start()
{
textln("Type \"test\" to run the break example.");
}
}
The continue keyword stops the current loop iteration in looping control blocks (while and for), which re-evaluates the conditional, and in for loops, also performs the stepping statement first.
action general a_testfor named "test for";
action general a_testwhile named "test while";
world
{
onAction(a_testfor)
{
textln("Printing every even digit from 0 to 20:");
local i;
for (i = 0; i <= 20; i = i + 1)
{
if (i % 2 != 0)
continue;
textln(i);
}
}
onAction(a_testwhile)
{
textln("Printing every even digit from 0 to 20:");
local previ;
local i = 0;
while (i <= 20)
{
previ = i;
i = i + 1;
if (previ % 2 != 0)
continue;
textln(previ);
}
}
start()
{
textln("Type \"test for\" or \"test while\" to run the continue example.");
}
}
There are a few other keywords that have special use in TAMEScript that don't quite fit into the other categories.
While world is used to mark the declaration of the world, it is used as the singular main identifier to refer to the world as an element in function calls.
/*
* This is an admittedly strange example.
*/
action general a_inc named "increment";
action general a_dec named "decrement";
action general a_quit named "quit";
container c_test
{
function checkWorld()
{
// check the value of x on the world's context.
if (world.x === 5)
textln("world.x is 5!");
else
textln("world.x is NOT 5!");
}
}
world
{
init()
{
// initialize x to 0.
x = 0;
}
onAction(a_inc)
{
textln("Incrementing world.x...");
x = x + 1;
}
onAction(a_dec)
{
textln("Decrementing world.x...");
x = x - 1;
}
onAction(a_quit)
{
quit;
}
start()
{
textln("Type \"increment\" to increment world.x");
textln("Type \"decrement\" to decrement world.x");
textln("Type \"quit\" to quit.");
}
afterSuccessfulCommand()
{
// call the container's function.
textln("world.x is " + x);
c_test.checkWorld();
}
}
While player is used to mark the declaration of players, it can be used as an identifier to mean "the current player." If there is no current player at the moment it is resolved, a FATAL error is thrown, so check for it using NoCurrentPlayer() if it is a possibility in your module.
action general a_switchplayer1 named "switch 1";
action general a_switchplayer2 named "switch 2";
action general a_inc named "increment";
action general a_dec named "decrement";
action general a_quit named "quit";
player p_player1
{
init()
{
count = 0;
}
}
player p_player2
{
init()
{
count = 0;
}
}
world
{
function printPlayer()
{
textln("current player is " + identity(player));
textln("player.count is " + player.count);
}
onAction(a_switchplayer1)
{
setPlayer(p_player1);
}
onAction(a_switchplayer2)
{
setPlayer(p_player2);
}
onAction(a_inc)
{
textln("Incrementing player.count...");
player.count = player.count + 1;
}
onAction(a_dec)
{
textln("Decrementing player.count...");
player.count = player.count - 1;
}
onAction(a_quit)
{
quit;
}
start()
{
setPlayer(p_player1);
textln("Type \"switch 1\" to switch to player 1.");
textln("Type \"switch 2\" to switch to player 2");
textln("Type \"increment\" to increment player.count");
textln("Type \"decrement\" to decrement player.count");
textln("Type \"quit\" to quit.");
printPlayer();
}
afterSuccessfulCommand()
{
printPlayer();
}
}
While room is used to mark the declaration of rooms, it can be used as an identifier to mean "the current room." If there is no current room at the moment it is resolved, a FATAL error is thrown, so check for it using NoCurrentRoom(...) if it is a possibility in your module. Remember: the current room is a state maintained by the current player.
action modal a_go named "go" uses modes "north", "south";
action general a_quit named "quit";
// prototype
room r_southroom;
room r_northroom
{
onModalAction(a_go, "south")
{
setRoom(player, r_southroom);
}
onModalAction(a_go, "north")
{
textln("Cannot go north.");
}
}
extend room r_southroom
{
onModalAction(a_go, "south")
{
textln("Cannot go south.");
}
onModalAction(a_go, "north")
{
setRoom(player, r_northroom);
}
}
player p_main
{
}
world
{
init()
{
setPlayer(p_main);
setRoom(player, r_northroom);
}
onAction(a_quit)
{
quit;
}
start()
{
textln("Type \"go north\" or \"go south\".");
textln("Type \"quit\" to quit.");
}
afterSuccessfulCommand()
{
// "room" is current room.
textln("Current room is " + identity(room));
}
}
The this keyword is a tricky one: it is used to refer to the element belonging to the current context. This means that this can be several different types, but you can count on its type to be the same as the element that contains the block of code that it is resolved in (for example, if it is used in an onAction() block on an object, its type is considered to be object).
This is especially important when using inheritance in defining objects. Since this means "the element that belongs to the current context," that means that it may not necessarily refer to the element that the code is written on.
action transitive a_take named "take";
action general a_quit named "quit";
object archetype o_ball
{
onAction(a_take)
{
if (hasObject(player, this))
textln("The current player already has this.");
else
{
giveObject(player, this);
textln("Taken.");
}
}
}
object o_red_ball : o_ball named "red ball", "ball"
{
onWorldBrowse()
{
textln("A red ball.");
}
onPlayerBrowse()
{
textln("A red ball.");
}
}
object o_blue_ball : o_ball named "blue ball", "ball"
{
onWorldBrowse()
{
textln("A blue ball.");
}
onPlayerBrowse()
{
textln("A blue ball.");
}
}
object o_green_ball : o_ball named "green ball", "ball"
{
onWorldBrowse()
{
textln("A green ball.");
}
onPlayerBrowse()
{
textln("A green ball.");
}
}
player p_main
{
}
world
{
init()
{
setPlayer(p_main);
giveObject(world, o_red_ball);
giveObject(world, o_blue_ball);
giveObject(world, o_green_ball);
}
function browseAll()
{
textln("The current player has:");
if (objectCount(player) > 0)
browse(player);
else
textln("NOTHING");
textln("The world has:");
if (objectCount(world) > 0)
browse(world);
else
textln("NOTHING");
}
onAction(a_quit)
{
quit;
}
start()
{
textln("Use \"take\" to take items.");
textln("Type \"quit\" to quit.");
browseAll();
}
afterSuccessfulCommand()
{
browseAll();
}
}
Most of the time in TAME, you're going to want to output text. While most text output is done by emitting text or textf cues via textln() or Textfln(), you can output passages by writing statements in this fashion, using a special ellipsis operator (...).
... "This is a passage of text to output.";
What this does is output code equivalent to:
Textfln(StrTrim(RegexReplace("\\s*\\n\\s*", " ", "This is a passage of text to output.")));
This is best used with raw strings, since the above code turns newlines and indentation into single spaces:
... `When in the Course of human events, it becomes necessary for one people to dissolve the
political bands which have connected them with another, and to assume among the powers of
the earth, the separate and equal station to which the Laws of Nature and of Nature's God
entitle them, a decent respect to the opinions of mankind requires that they should declare
the causes which impel them to the separation.`;
You may notice that there's a textfln rather than a textln in the "equivalent code" blurb - passages are output as a textf cue as though it were in a Textfln() call.
The expression after the ... is just that: an expression. You can even do the following:
... 2 + 2;
...which would predictably output "4".
You can also output a string with parameterized text like StrParam() by appending a comma and a list containing the elements to insert. If the parameter list is not a list, it is upgraded to a single-element list.
... "Hello! My name is {0}!", ["Bob"];
Which means the output code is equivalent to:
Textfln(StrParam(StrTrim(RegexReplace("\\s*\\n\\s*", " ", "Hello! My name is {0}!")), ["Bob"]));
Used properly, this could make a lot of code with a lot of text to output easier to read.
world
{
start()
{
... "This is a passage.";
... ""; // blank newline
... `Four score and seven years ago our fathers brought forth on this
continent, a new nation, conceived in Liberty, and dedicated to the
proposition that all men are created equal.`;
... "";
... `Now we are engaged in a great civil war, testing whether that nation,
or any nation so conceived and so dedicated, can long endure. We are met
on a great battle-field of that war. We have come to dedicate a portion
of that field, as a final resting place for those who here gave their lives
that that nation might live. It is altogether fitting and proper that we
should do this.`;
quit;
}
}
Since you can have multiple elements that more than likely have the same types of characteristics or behavior with certain actions, it might be useful to define them hierarchically. TAMEScript has a mechanism to define these relationships, called inheritance.
Elements in TAME (that are not the world) can specify a parent element of the same type to inherit the defined blocks and functions from:
object o_car named "car"
{
init()
{
driven = false;
}
onAction(a_drive)
{
if (driven)
textln("I already drove it.");
else
textln("You drive the car. Wheee!");
driven = true;
}
onAction(a_examine)
{
textln("Looks like a car.");
}
}
object o_sedan : o_car named "sedan", "car"
{
override onAction(a_examine)
{
textln("Looks like a sedan.");
}
}
In the above example, the object o_sedan inherits from o_car. The object o_sedan is now functionally and structurally equivalent to:
object o_sedan named "sedan", "car"
{
/* Inherited stuff starts here */
init()
{
driven = false;
}
onAction(a_drive)
{
if (driven)
textln("I already drove it.");
else
textln("You drive the car. Wheee!");
driven = true;
}
/*******************************/
onAction(a_examine)
{
textln("Looks like a sedan.");
}
}
There is no limit to how deep an inheritable hierarchy can go, but it's still best to use identifiers and schemes that make sense in order to keep it from being overwhelming.
object o_allobjects;
object o_keys : o_allobjects;
object o_orange_key : o_keys;
object o_blue_key : o_keys;
room r_allrooms;
room r_livingroom : r_allrooms;
room r_bedroom : r_allrooms;
room r_master_bedroom : r_bedroom;
In the previous example, one of the entry blocks was changed in the child object of o_car. In order to denote that it is being replaced, the new function or entry block must be prefixed with the override keyword.
room r_livingroom
{
function examineRoom()
{
textln("You are in the living room.");
}
onAction(a_lookaround)
{
examineRoom();
}
}
room r_cozylivingroom : r_livingroom
{
override function examineRoom()
{
textln("You are in the COZY living room.");
}
}
NOTE: If you attempt to define an existing entry point or function within an element's lineage without using override, the TAMEScript compiler will give you an error.
The keyword archetype can be used to create elements that do not hold a context, but instead have common functions and entry points among its descendants. Archetypes can also inherit from other elements of the same type. Archetyping is useful when you have multiple elements with similar behavior, with small changes among its children.
For example, if you have a set of keys, it might be preferable to create an archetype key that all keys inherit from, and the only thing different would be the material or look (since you can check types).
action general a_quit named "quit";
action general a_lookaround named "look around", "look";
action transitive a_examine named "examine", "look at", "x";
action strict ditransitive a_unlock named "unlock", "open" uses conjunctions "with";
// All examinable objects.
object archetype o_examinables
{
// called on first examine.
// override this.
function examineFirst()
{
textln("[EXAMINE FIRST]");
}
// called on subsequent examine.
// override this.
function examineAgain()
{
textln("[EXAMINE AGAIN]");
}
onAction(a_examine)
{
if (examined)
examineAgain();
else
examineFirst();
examined = true;
}
}
// All keys.
object archetype o_keys : o_examinables
{
}
object o_wooden_key : o_keys named "wooden key", "key"
{
override function examineFirst()
{
textln("It's a wooden key.");
}
override function examineAgain()
{
textln("It's still a wooden key.");
}
onWorldBrowse()
{
textln("A wooden key is here.");
}
}
object o_steel_key : o_keys named "steel key", "key"
{
override function examineFirst()
{
textln("It's a steel key.");
}
override function examineAgain()
{
textln("It's still a steel key.");
}
onWorldBrowse()
{
textln("A steel key is here.");
}
}
object archetype o_doors : o_examinables
{
// fallback on an incorrect key.
onActionWithAncestor(a_unlock, o_keys)
{
textln("This key isn't working.");
}
// fallback on an incorrect anything.
onActionWithOther(a_unlock)
{
textln("I can't unlock this with that.");
}
}
object o_wooden_door : o_doors named "wooden door", "door"
{
override function examineFirst()
{
textln("It's a wooden door.");
}
override function examineAgain()
{
textln("It's still a wooden door.");
}
onWorldBrowse()
{
textln("A wooden door is here.");
}
onActionWith(a_unlock, o_wooden_key)
{
textln("You unlock the wooden door.");
}
}
object o_steel_door : o_doors named "steel door", "door"
{
override function examineFirst()
{
textln("It's a steel door.");
}
override function examineAgain()
{
textln("It's still a steel door.");
}
onWorldBrowse()
{
textln("A steel door is here.");
}
onActionWith(a_unlock, o_steel_key)
{
textln("You unlock the steel door.");
}
}
world
{
start()
{
giveObject(world, o_wooden_key);
giveObject(world, o_steel_key);
giveObject(world, o_wooden_door);
giveObject(world, o_steel_door);
textln("You are in a room with some keys and doors.");
textln("You can EXAMINE things or UNLOCK things.");
browse(world);
}
onAction(a_lookaround)
{
browse(world);
}
onMalformedCommand()
{
textln("I don't understand.");
}
onIncompleteCommand(a_examine)
{
textln("Examine what?");
}
onMalformedCommand(a_examine)
{
textln("I don't understand. That's not a thing I can examine.");
}
onUnhandledAction(a_examine)
{
textln("I can't examine that.");
}
onIncompleteCommand(a_unlock)
{
textln("Unlock what with what?");
}
onMalformedCommand(a_unlock)
{
textln("I don't understand. That's not a thing I can unlock.");
}
onUnhandledAction(a_unlock)
{
textln("I can't unlock that.");
}
onAction(a_quit)
{
quit;
}
}
Note that just because an element can inherit functions and entry blocks from other elements, it does NOT inherit context variables or treat context variables as though they are shared in the hierarchy - each element (that is NOT an archetype) still has its own context. This means that nothing at runtime is shared, only the code that executes.
action general a_quit named "quit";
action transitive a_use named "use";
// All doors.
object archetype o_door
{
init()
{
opened = true;
}
function getStateName()
{
return opened ? "OPEN" : "CLOSED";
}
onAction(a_use)
{
opened = !opened;
}
}
object o_red_door : o_door named "red door", "door"
{
onWorldBrowse()
{
textln("The red door is "+getStateName()+".");
}
}
object o_blue_door : o_door named "blue door", "door"
{
onWorldBrowse()
{
textln("The blue door is "+getStateName()+".");
}
}
world
{
start()
{
giveObject(world, o_red_door);
giveObject(world, o_blue_door);
textln("You can USE the doors.");
browse(world);
}
onAction(a_quit)
{
quit;
}
afterEveryCommand()
{
browse(world);
}
}
While technically not part of the language spec, the TAMEScript compiler makes use of a few "preprocessor" directives that either import other code from other script files, or use macros to define sets of tokens in the script.
Preprocessor directives must occupy a line - the directive plus parameters are terminated by a newline.
The #include directive includes the contents of the specified file. The filename is provided as a string parameter, and can either be a relative file path from the file that contains the directive or an absolute path. Files can be included more than once - use this with caution!
NOTE: The Java implementation of the compiler can also resolve resources off of the classpath by prefixing the path string with classpath:. The include behavior can also be replaced entirely in the Java implementation.
#include "rooms.tscript"
#include "objects/keys.tscript"
The #define directive defines a single-token macro that expands to a series of other tokens. This is also useful for creating defines and testing if they were defined later. They may also expand to zero tokens. All macro tokens are CASE SENSITIVE!
NOTE: Unlike the C-language preprocessor, this does not create macro functions.
#define GREETING_TEXT "Hello."
#define TWO_PI ( PI() * 2 )
#define FILE_WAS_INCLUDED
The #undefine directive removes a previously defined macro. All subsequent uses of that macro are treated as though they were never defined.
#define GREETING_TEXT "Hello."
#undefine GREETING_TEXT
textln(GREETING_TEXT); // compiler thinks GREETING_TEXT is a variable
The #ifdef directive includes the next series of lines if the following macro was defined, until it reaches an #endif directive.
#define LINES
#ifdef LINES
textln("LINES was defined.");
#endif
The #ifdef directive includes the next series of lines if the following macro was NOT defined, until it reaches an #endif directive.
#define STUFF
#ifndef LINES
textln("LINES was not defined.");
#endif
The #else directive ends the most recently started "if" directive block and provides an alternate section if the first "if" is not processed.
#define LINES
#ifdef LINES
textln("LINES was defined.");
#else
textln("LINES was NOT defined.");
#endif
The #endif directive ends the most recently started "if" or "else" directive block.
#define LINES
#ifdef LINES
textln("LINES was defined.");
#endif
#define LINES
#ifdef LINES
textln("LINES was defined.");
#else
textln("LINES was NOT defined.");
#endif
5.1. Comments