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:
- Rules, rules, rules!
A very
basic introduction to the very basics of rule-writing.
- Parameters and Return Values
How to send values from one rule to another.
- Operators
Math and string concatenation.
- Conditional Branching
How to make your rules smarter by performing a given
action only if a certain condition is true.
- Looping
How to perform actions repeatedly.
- Arrays
The filing-cabinet type, for managing lists of
values.
- Built-In Functions
Built-in functions you can use.
- 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:
- 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)!
- The rule's identifier must immediately follow the
rule keyword. This identifier must be unique.
- 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.
- 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!
- All rules are terminated by the 'end' keyword,
just like things, templates, and categories.
- 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:
- eq -- test to see if two values are equivalent.
- ne -- test to see if two values are not equivalent.
- gt -- test to see if one value is greater than
another.
- lt -- test to see if one value is less than
another.
- ge -- test to see if one value is greater than
or equal to another.
- le -- test to see if one value is less than or
equal to another.
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:
- Lowest precedence: eq ne gt lt ge le
- + -
- * / %
- Highest precedence: ^
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).
-
Add( category, item )
Add( category, item, weight )
The "Add" function adds the given item to the given
category. The 'item' must be either a thing, a
category, a rule, or the "null" keyword. The 'weight'
parameter is optional, and if specified, it is used
as the weight for the item in the category (see the
description of weighting in the section on
categories). If
the weight is not specified, it is assumed to be 1.
-
Any( category )
This function returns an item at random from the given
category, taking into account the weighting of the
items. The item is not removed from the category. See
also GetByWeight and
TotalWeightOf.
-
AttributeNameOf( thing, index )
This function returns the name of the attribute at
the given index. The first attribute is at index 0,
the second is at index 1, etc. If there is no attribute
at the given index, this function returns the empty
string "".
-
AttributeOf( thing, name )
This function returns the attribute with the given
name in the given thing. If the attribute does not
yet exist, it is created. This value may be assigned
to.
-
AttributeValueOf( thing, index )
This function returns the value of the attribute at
the given index. The first attribute is at index 0,
the second is at index 1, etc. If there is no attribute
at the given index, this function returns null.
-
ConvertUnits( number, units )
Converts the given number from whatever units it is
currently in to the units specified by the string
"units". If the units are incompatible, you'll get
an error, otherwise, the converted number is
returned.
-
Count( category )
Returns the number of items in the given category.
-
Dice( count, type )
Returns a new dice value with the given count and
type of dice.
-
DiceCount( value )
If value is a dice value, this function returns the "count"
portion of the value (ie, if the value was 10d6, this function
returns 10). If the value was a number but not a dice value,
this function returns 1.
-
DiceType( value )
If value is a dice value, this function returns the "type"
of the dice (ie, if the value was 10d6, this function returns
6). If the value was a number but not a dice value, this
function returns the number itself.
-
DiceModType( value )
If value is a dice value, this function returns the type of the
modifier. This will be "*" for a multiplicative modifier or
"+" for an additive modifier. (ie, if the value was 10d6+15, this
function returns "+").
-
DiceModifier( value )
If value is a dice value this function returns the modifier portion
of the dice value. Ie, if the value was 10d6+15, this function
returns 15.
-
Duplicate( thing )
Duplicate( category )
Returns a copy of the given thing or category.
-
Empty( category )
Returns true if the given category has no items, and
false otherwise.
-
Eval( value )
If the value is a dice value, the dice are rolled and
the result is returned, otherwise the value itself is
returned. See also Random.
-
Exists( category, item )
Returns true if the actual item given (not a
duplicate) exists in the
given category, and false otherwise.
-
Floor( number )
Returns the first integer number less than or equal
to 'number'.
-
Get( category, index )
Returns the item at the given index in the given
category. The first item is at index 0. If there
is no item at the given index, "Get" returns null.
-
GetByWeight( category, weight )
Returns the item "at" the given weight in the category.
See also TotalWeightOf
and Any.
-
Has( thing, name )
Returns true if the given thing has an attribute named
'name', otherwise returns false.
-
IndexOf( category, item )
Returns the index of the given item in the given
category, or -1 if the item does not exist in the
category. See also Get and
WeightOf.
-
Instr( string, substr )
Instr( string, substr, pos )
Returns the index of the first instance of substr
within the given string. If the 'pos' parameter is
specified, the index of the first instance of substr
after the index specified by pos is returned. If
substr cannot be found, Instr returns -1.
-
Int( number )
Returns the integer nearest to number.
-
Intersection( category1, category2 )
Returns the "intersection" of the two categories. The
intersection is the group of items that exist in both
categories. See also Subtract
and Union.
-
Length( string )
Length( array )
If the parameter is a string, returns the number of
characters in the string. If the parameter is an
array, the number of elements in the array is
returned.
-
Ln( number )
Returns the natural logarithm of the given number.
-
LowerCase( string )
Returns the given string, with all characters converted to
lower-case. See also UpperCase.
-
MagnitudeOf( number )
Returns the number, stripped of any units it might
have had. See also UnitsOf.
-
Mid( string, start )
Mid( string, start, length )
Returns all characters in the string starting at the
given 'start' index, or, if the 'length' is specified,
it returns a string containing all characters from
start to the length specified.
-
NewArray()
NewArray( length )
If no parameters are specified, a new array with no
elements is returned. If a length parameter is
specified, the new array will have that many elements
(all initialized to null).
-
NewCategory()
Returns a new category, empty of all items.
-
NewThing()
Returns a new thing, with no attributes.
-
Parameter( index )
Returns the parameter to the rule at the given
index, with the first parameter existing at index 0.
Use this function to get parameters of variable-arity
rules.
-
Print( string )
Print( number )
Displays the given value. This depends on how the
application implements a "console" as to how the
value is displayed. Some applications may choose not
to implement a console -- in this case, this function
does nothing.
-
Random( number )
If number is a dice value, the dice value is rolled
and the result returned. If the number is an integer,
a random number between 0 and number-1 (inclusive) is
returned. See also Eval.
-
Remove( category, item )
The item is removed from the category, if it existed
in the category. If it did not, this function does
nothing. See also RemoveAll.
-
RemoveAll( category )
Removes all items from the given category, leaving
it empty. See also Remove.
-
Replace( string, pattern, replacement )
Replace( string, pattern, replacement, pos )
Replaces the first instance of pattern in string with
replacement. If pos is specified, then the first
instance at or after pos is replaced with replacement.
The new string is returned. If no replacements were
made (because pattern did not exist in string), then
the original string is returned.
-
SearchCategory( category, attribute )
SearchCategory( category, attribute, value )
Searches the given category for the first thing with
an attribute with the specified name. If the value
parameter is also specified, then the attribute must
have the specified value as well.
-
SetUnits( number, units )
Returns a new number with "units" as its units. If
the "units" string does not specify a declared unit
type, an error is generated.
-
Sort( array, comparer )
Sorts the given array, and returns it (though the
parameter is changed as well). The comparer must be
a rule that you have defined that accepts two
parameters, and returns -1 if the first is less than
the second, 0 if they are equivalent, or 1 if the first
is greater than the second. This rule is used to
determine the sort order of the elements in the array.
-
Sqrt( number )
Returns the square root of the given number.
-
Subtract( category1, category2 )
Subtracts category2 from category1 and returns the
new category containing the difference. The difference
is defined to be all items in category1 that do not
exist in category2. See also Intersection
and Union.
-
TotalWeightOf( category )
Returns the total weight of all elements in the
category. See also Any and
GetByWeight.
-
Union( category1, category2 )
Returns the union of the two categories. The union
is defined to be all elements that exist in either
category (with no duplicates). See also
Intersection and
Subtract.
-
UnitsOf( number )
Returns a string describing the units of the given
number, or "" if the number has no units. See also
MagnitudeOf.
-
UpperCase( string )
Returns the given string, with all characters converted to
upper-case. See also LowerCase.
-
WeightOf( category, index )
Returns the weight of the item at the given index
in the given category. The first item in a category
is at index 0. If no item exists at the given index,
0 is returned.
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!