Basilisk Script

Whereas things form the building blocks of a Basilisk-defined universe, and categories form the glue that holds those blocks together, rules define how the things and categories interact and behave. They are also used to define "functional" values -- those values (like ability score bonuses in D&D) that are computed, rather than looked up.

This document will discuss the syntax of rules in Basilisk, and will demonstrate through various examples how they are written and used.

Please click on a chapter heading:

  1. Rules, rules, rules!
    A very basic introduction to the very basics of rule-writing.
  2. Parameters and Return Values
    How to send values from one rule to another.
  3. Operators
    Math and string concatenation.
  4. Conditional Branching
    How to make your rules smarter by performing a given action only if a certain condition is true.
  5. Looping
    How to perform actions repeatedly.
  6. Arrays
    The filing-cabinet type, for managing lists of values.
  7. Built-In Functions
    Built-in functions you can use.
  8. Conclusion
    Wrapping things up.

1. Rules, rules, rules!

Here's an example of a very simple (and quite useless) rule:
rule myVeryFirstRule()
/* this is a multi-line
comment! It can stretch for as long as
you want. */

x = 2 + 2;

// this is a single-line comment!
end
This example demonstrates some very basic, but very important, points:
  1. All rules are declared with the 'rule' keyword. There is only one way to declare rules (unlike things and categories, which may even be declared anonymously)!
  2. The rule's identifier must immediately follow the rule keyword. This identifier must be unique.
  3. Note the empty parenthesis after the identifier. You must always put these parenthesis here. In a later example I'll show you exactly what these are for.
  4. Comments may be specified either between /* and */ or on one line following //. Comments are text that you may insert liberally into your rules to help people that later look at them to understand what is happening. Use comments often, and use them well!
  5. All rules are terminated by the 'end' keyword, just like things, templates, and categories.
  6. All statements (except those ending with the 'end' keyword) must end with a semicolon ';'. You'll run into all kinds of problems if you forget even one semicolon, anywhere! In the same vein, if you put a semicolon where you're not supposed to (like after the 'end' keyword), you'll get problems, too.
That little letter 'x' is what is called a "variable." Those of you who didn't sleep through basic algebra in high school will be familiar with that term. For the rest of you, a variable is simply place that you can put values. In Basilisk, a variable may hold any type of value, be it numeric, string, boolean, thing, category, rule, or otherwise. And in the example shown above, the variable x "gets" (that's how you read that '=' sign) the result of 2 + 2, or 4.

Notice that this also demonstrates how to compute values. You can use '+' and '-' for addition and subtraction (respectively), '*' for multiplication, '/' for division, '^' for exponentiation, and '%' for modulus division (where you get the remainder instead of the result). Also, anywhere you would normally put a number in a formula, you can also put a variable (as long as the variable contains a value appropriate to the operation being performed). For example:

rule myNextRule()
x = 15;
y = x * 3;
end
This example assigns the number 15 to x, and then assigns x times 3 to y. Since x is 15, y will get the value 45 (15 times 3). Make sense? Great! We'll talk more about math in the section on operators.

One other thing. Although a rule will always terminate when it reaches the 'end' keyword at the end of a rule, there are times when you want to exit a rule early. You can do this with the "exit rule" statement:

rule exitDemo()
/* stuff here */

exit rule;

/* more stuff here */
end
The rule will end when it reaches the "exit rule" statement.

2. Parameters and Return Values

Well. That's all fine and dandy, right? You know how to add 2 and 2. Now, suppose you had something like this:
rule hereIsAnUglyOne()
y = 15;
x = ( ( 3 * y ) / 2 ) + 1;
/* do something with x here */
x = ( ( 3 * y ) / 2 ) + 2;
/* do something else with x here */
x = ( ( 2 * y ) / 2 ) + 1;
/* do something else with x here */
end
What if you could simplify this a bit? What if there were a way to just send the values that change in each formula to another rule and have that rule return the result?

You've probably guessed that there is, indeed a way to do it. Observe this next example:

rule doMyFormula( a b c )
doMyFormula = ( ( a * b ) / 2 ) + c;
end

rule hereIsAnUglyOne()
y = 15;
x = doMyFormula( 3, y, 1 );
/* do something with x here */
x = doMyFormula( 3, y, 2 );
/* do something else with x here */
x = doMyFormula( 2, y, 2 );
/* do something else with x here */
end
Hope you didn't blink, or you would have missed it! What we did here is define a new rule, called 'doMyFormula' that takes 3 parameters. A parameter is a value that you can send to a rule, and rule's can accept any number of parameters.

The three variables ('a', 'b', and 'c') in the parenthesis beside 'doMyFormula' are the parameter list of 'doMyFormula'. In 'hereIsAnUglyOne', the three values in the parameter list beside doMyFormula get mapped, value-by-value, to those three variables ('a', 'b', and 'c').

By assigning the result of the formula to 'doMyFormula', we are essentially saying that 'doMyFormula' will return the calculated value as it's return value. You'll see that we actually assign that return value to x three times in 'hereIsAnUglyOne'.

Even though a given rule may only have 3 arguments, you can pass as many parameters to it as you want, or as few. If you pass fewer parameters than the rule expects, the remaining arguments are automatically initialized to null. You may use the Parameter function to access parameters beyond those declared by the rule.

You'll see a lot of parameters and return values before this documentation is done.

3. Operators

Operators are things like '+' and '-'. In Basilisk, you can use operators to perform mathematical calculations, and to perform string concatenation.

Here's an example of all the mathematical operators:

rule operatorDemo()
x = 3 + 2; /* gets 5 */
x = 3 - 2; /* gets 1 */
x = 3 * 2; /* gets 6 */
x = 3 / 2; /* gets 1.5 */
x = 3 ^ 2; /* gets 3 squared, or 9 */
x = 3 % 2; /* gets 1, or 3 mod 2 */
end

The '+' operator may also be used to catenate two strings together, like so:

rule operatorDemo2()
x = "Hello " + "World"; 
/* gets "Hello World" */
x = "I am " + 26 + " years old"; 
/* gets "I am 26 years old" */
end
Notice that when you "add" strings and numbers together, the numbers are converted to strings and catenated into the final result.

Other operators are used to test equivalence. These are:

Each of the above operators returns a boolean (true or false) value as the result. Here's an example:
rule comparisonDemo()
x = 3 eq 4; /* gets false */
x = "nice" ne "mean" /* gets true */
x = 7 le 9; /* gets true */
end

There are two other operators you can use. These are and and or. The and operator will return true only if both of the operands are true. The or operator will return true if either of the operands are true.

rule conjunctionDemo()
x = 3 eq 4;
y = 7 le 9;
z = x or y; /* gets true */

x = 3 lt 2;
y = "b" eq "c";
z = x and y; /* gets false */
end
There is also this little issue called "operator precedence." What it means is that, for example, before you perform addition, you perform all multiplications first. In other words, 'multiplication' has a higher precedence than 'addition'.

Precedence in Basilisk behaves just as it does in math, with expressions being evaluated left-to-right in order of precendence. To wit:

Also, expressions in parentheses are performed as if they had an even higher precedence. For example:
rule operatorPrecedenceDemo()
x = 3 ^ 2 + 1; /* x gets 10 */
x = 3 ^ ( 2 + 1 ); /* x gets 27 */
end
In the first example, "3 ^ 2" is evaluated first, and then 1 is added to the result. In the second, "2 + 1" is evaluated, and 3 is then raised to the resulting power.

4. Conditional Branching

Sometimes you only want a particular statement to execute if a certain condition is true. For instance, you only want your age to increase IF it is your birthday (and even then, some people would stop it if they could). Basilisk supports then kind of "branching" via an 'if' statement.

For example:

rule ifDemo( something )
if something eq 5 then
/* do some processing */
end
end
The above example will test to see if the parameter 'something' equals 5, and if it does, it will perform some sequence of actions.

Sometimes you want one thing done if a condition is true, and another if it is false. You can use the 'else' clause to accomplish this:

rule ifDemo2( something )
if something eq 5 then
/* do some processing */
else
/* do something else */
end
end
And sometimes, you want 'a' if 'b', or 'c' if 'd', or 'e' if 'f', or otherwise just do 'g', like so:
rule ifDemo3( something )
if something eq 5 then
/* do some processing */
elseif something lt 2 then
/* do something else */
elseif something lt 10 then
/* do something else */
else
/* do the default action */
end
end
The 'else' clause is always optional, and need not be specified if it is not needed.

There is one other way to do conditional branching in Basilisk. This is via the 'case' statement. Sometimes you just want to perform some action if something has a particular value. The 'case' statement is ideal for this:

rule caseDemo( something )
case something
is 1 then
/* do something */
is 2 then
/* do something */
is 3 then
/* do something */
is 4 then
/* do something */
is 5 then
/* do something */
default
/* do something if it is none
of the specified values */
end
end
Notice that the case statement ends with the 'end' keyword. Also, the 'default' section is optional -- you don't need to specify it if you don't need it. You can also test non-equivalency with case statement, as follows:
rule caseDemo( something )
case something
is 1 then
/* do something */
is not 2 then
/* do something */
end
end
This has limited uses, however, and actually is almost equivalent to the 'default' statement in most cases.

5. Looping

Sooner or later, you'll find that you want to perform some action over and over again. You'll want to generate ten random magic items, or create ten random characters. Sure, you could just write the code to generate one item or character, and then copy it 10 times, but what happens what you suddenly decide you need 20, or 100? That's right, you turn to loops.

There are three types of loops in Basilisk, 'for' loops, 'while' loops, and 'do' loops. Let's look at them in that order.

For Loops

A for loop looks like this:

rule forLoopDemo()
x = 0;
y = 0;
for i = 1 to 10 do
x = x + 1;
y = y + i;
end
end
This example first sets the variable x to be 0. Then, for all values of i from 1 to 10, the statement "x = x + 1" is executed. By the end of the 10th iteration, the variable x contains the number 10 (it was incremented by 1, 10 times).

The value of y is a little trickier. The first time through the loop, the value of i is 1, and that value is added to y (initially 0), making 1. The second time through the loop, i is 2, which is added to y (now 1), making 3. The third time through the loop, i is 3, and this is added to y, making 6. This continues through 10 iterations, and at the end, y is 55, the sum of all the numbers from 1 to 10.

There's nothing that says a for loop needs to start at 1. You can start it at 5, or 15, or -3. However, since the loop goes upward, if you specify a "to" number that is less than the "from" number, the loop will not execute even once.

There is one other format for a "for" loop: you can use it to iterate through all the values in a category. Here's an example:

attribute cost number

category testCategory
{ .cost 5 }
{ .cost 1 }
{ .cost 11 }
{ .cost 4 }
end

rule forLoopDemo2()
total = 0;
for i in testCategory do
total = total + i.cost;
end
end
In this example, the variable 'i' will, for each time through the loop, be a different one of the members of the 'testCategory' category. The first time through the loop, i is the first thing in the category, the second time, it is the second thing, and so on. One caveat, however: if the category contains a "null" member, the loop will stop processing when it reaches it.

Notice also how attributes of things are referenced -- with a '.' followed by the attribute name.

While Loops

A "while" loop is a bit different than a "for" loop. Essentially, it performs an action "while" a given condition is true. Here's an example:

rule whileLoopDemo()
x = 0;
while x lt 10 do
x = x + 1;
end
end
This example increments x by 1 "while" x is less than 10. Since x starts as 0, the loop will execute 10 times.

The condition is evaluated each time the loop is executed, before anything in the loop is executed. This means that if the condition is not true when the loop is first encountered, the loop will not execute.

Do Loops

A "do" loop is much like a "while" loop, except that the condition is tested at the end of the loop, instead of the beginning. This means that the loop will always execute at least once. Here's an example:

rule doLoopDemo()
x = 0;
do
x = x + 1;
loop while x lt 10;
end
This example does exactly what the "while" loop example does -- it counts to ten.

Exiting Loops Early

A final note about loops -- loops will exit when the specified condition is met, but there may be times when you want to terminate the loop early. You can do it with the "exit loop" statement, much like the "exit rule" statement described earlier. Here's an example:

rule exitLoopDemo()
x = 0;
while true do
x = x + 1;
if x ge 10 then
exit loop;
end
end
end
In this (rather contrived) example, notice that the loop condition is always true, meaning that the while loop will never exit! However, the 'if' test within the loop will ensure that when/if the variable x is greater than or equal to 10, the "exit loop" statement will be executed, which will terminate the loop.

The "exit loop" statement may be used to exit out of any of the loop types (for, while, and do).

6. Arrays

Sometimes you may have a list of values that you want to treat as a single entity, like the names of all your friends. Such a grouping is especially useful if you want to sort those names and get them in alphabetical order. You can use arrays for this.

Arrays are created by calling the built-in function, NewArray. Here's an example:

a = NewArray();
This example creates a new array with no elements. Elements of an array are referenced by using square brackets '[' and ']', with the index of the element you want within the square brackets, like this:
a[5] = "Hello";
This assigns the word "Hello" to the element at index 5 of the array. Note that the first element in an array is at index 0, not 1. Also, if the array is not large enough to have an element at the index given, the array is automatically grown to fit it. For example:
a = NewArray();
a[3] = "Hello";
a[15] = "World";
Here, an empty array is created, with no elements. The second statement assigns "Hello" to index 3, which causes the array to grow to include index 3. The third statement assigns "World" to index 15, and again the array is grown. All elements not explicitly assigned to (like element 1, or 4, etc. in the example above) are given the value "null".

If you know how big you want your array to be, you can make your rules a little faster by specifying the initial size of the array, as follows:

a = NewArray( 15 );
a[3] = "Hello";
a[15] = "World";
In this example, the array is initialized with 15 elements (indices 0 to 14). When index 3 is assigned to, the array doesn't have to grow because index 3 is already included. However, index 15 (the 16th element, because the first is index 0), is not included, so the array has to grow by 1 element to include index 15.

Lastly, you can sort arrays using the built-in function Sort.

7. Built-In Functions

Basilisk comes all set with quite a few built-in "functions." If the term "function" is unfamiliar to you, think of it as a rule.

What follows is a list of all the built-in functions in Basilisk, and what parameters they take, and what values they return (if any).

8. Conclusion

Well, that about wraps that up. You should now be an expert on rule-writing, right? *chuckle*

If you ever have any questions while writing your rules for your data, feel free to drop me a line and I'll do my best to answer them. E-mail me at minam@rpgplanet.com. You can also post questions and comments to the Basilisk message board, accessible from here.

Thanks!