CELL RUNNER DOCUMENTATION
Cell Runner has a built-in programming language that can be used to customize your worlds, maps, sprites, and other aspects of your game to your liking. The language is reasonably simple to understand and yet offers decent power and flexibility. Experienced programmers shouldn't have much trouble getting familiar with it, and first-time programmers may find it to be a good introductory language to use to learn how to program.
The language is designed in conceptual layers, and so will be explained as such, one layer at a time from the most fundamental syntax to the included library of functions for creating your game.
At its most fundamental, writing code consists of combining five basic types of elements: numbers, strings, symbols, and nil. (There are other fundamental types such as functions, enumerations, and structures, but those will be discussed later.)
A number is just that. It can be positive, negative, or fractional. E.g.,
5 -18.2 45.9667
A string is a collection, or "string" of characters. It is represented in the language by enclosing the characters in double-quotes. E.g.,
"Hello" "The quick brown fox jumped over the lazy dogs." "You have been eaten by a grue." ""
The last one is an empty string, zero characters long.
If you want to include the double-quote character in a string, proceed it with a back-slash. E.g.,
"She said, \"Hello\" to me."
A symbol is used to represent something else, such as a number, string, function, or various other possible types of values. A number of built-in symbols are provided for you, but you can also create your own with the assignment operator.
Often, other languages call this concept a "variable." While that's fine, "symbol" is a better name for this language because, usually, symbols are not variable. They can be re-assigned to something new altogether, but their given value at any one time isn't mutated. (There are exceptions to this as you'll see later on.)
They can be made up of the letters A - Z, upper- or lower-case, numbers and the underscore, but cannot start with a number. They can be any length and are case-sensitive. E.g.,
mySymbol width2 this_is_a_very_long_symbol_name_but_theres_no_length_limit_so_who_cares R2D2
Nil is another basic type of element. It can be thought of as a special symbol that means "no value" or "nothing." It is always written in all lower-case, i.e., nil.
Human-readable comments, annotations, and any other text can be inserted into a program by using the tilde (~) to start your comment, and a second tilde to end it. They can be as short or long as you like, and can span multiple lines. E.g.,
~This is a comment. It spans multiple lines.~
A statement is a complete conceptual instruction given to the computer, and a program consists of a collection, or a list, of statements. They are evaluated one-by-one in the order they are given. (However, as you'll see, various language features allow the evaluation of the program to jump around to different points in the program.) A statement is always ended with a semicolon (;) and are typically written one statement per line (though they don't have to be.)
All of the above, numbers, strings, symbols, and nil, are put together into these statements with operators. That is, operators are the glue that bind these elements together. As Cell Runner evaluates (or processes) your programs, it looks for an operator, what's on each side of it, and computes the result. E.g.,
5 + 6; ~This evaluates to 11~
evaluates by taking the addition operator (+), adding together what's on either side of it and coming back with (or "returning") the result. Thus, the above operator and operand group is a complete statement and therefore is concluded with the semicolon. Several operators can be combined in one statement. E.g.,
5 + 6 * 8 - 4 / 2;
is a valid statement. It performs the multiplication first (6 * 8 = 48), then the division (4 / 2 = 2), then the addition (5 + 48 = 53) and the subtraction (53 - 2 = 51).
The complete set of mathematical operators are
and they all work as expected for performing basic arithmetic. The modulo, if you're not familiar with it, computes the remainder of a division operation. For example, when dividing 19 by 4, you get 4 with a remainder of 3. So,
19 % 4; ~returns 3~
You are only allowed to use numbers, or symbols of numbers, as operands to these mathematical operators.
There is one operator that works on strings.
The concatenate operator joins two strings together into one longer string. E.g.,
"Hello, " $ "world!"; ~returns "Hello, world!"~
Both operands of the concatenate operator must be strings or symbols of strings.
Frequently, when writing a program, we want to look at different values and compare them to each other for the purpose of branching to this or that section of the program depending on the result of the comparison. This is accomplished with the equality operators. They return a number that is either 0 which means false, or non-zero which means true. (Although I say non-zero, these operators will always return 1 for true. However, non-zero is a more correct general term because any non-zero number is considered "true" by the language.)
Many other languages use special symbols such as "true" and "false" to represent truthiness instead of non-zero and zero. I considered this carefully when designing this language but ultimately decided against it. I decided it is ultimately unnecessary, and adds more complexity and types.
The equality operators are,
where = returns true if the operands are considered equal to each other and ! returns true when the operands are not considered equal to each other. In other words, given the same operands, = and ! will always return the opposite result. The equality operator can be defined as follows,
The relational operators compare the relationship between numbers. They cannot be used on any other type. They are,
and should do as you expect.
4 < 5; ~returns true~ 4 > 5; ~returns false~ 5 < 5; ~returns false~ 5 <= 5; ~returns true~ 6 >= 4; ~returns false~
Often, you want to combine two or more equality and relational operators to logically combine in different ways. The logical operators achieve this. For example, suppose you wanted to know if the symbol x is greater then 5 and if the symbol y is less than 7. That is, if BOTH x is greater than 5 AND y is less than 7. If they're both true, then we want to return true, but if one is true and the other isn't, or if they're both false, return false. For this, you'd use the logical-and operator, &, as follows,
x > 5 & y < 7;
and the statement will evaluate as desired. The complete list of logical operators are,
The difference between logical-or and logical-xor is that logical-or translates to "if one or the other or both are true, than return true otherwise return false if they're both false" and logical-xor translates to "return true if one or the other are true, but not if they're both true or if they're both false." Think of logical-xor as like choosing a side dish with your sandwich at a restaurant. The waiter asks you if you would like soup or salad with your sandwich, meaning you can choose either one or the other side dish but you can't have both.
The negated versions of each logical operator are simply logically inverted from their non-negated counterpart. For example, logical-nand can be translated to "return FALSE if either one or the other or both are true, otherwise return TRUE if they're both false."
Finally, the logical-not operator logically negates its operand. That is, false becomes true and true becomes false. It is unique in that this operator alone only takes one operand whereas all the others take two, one operand on each side of the operator. In the case of logical-not, the operand is placed just after the ^. E.g.,
^1; ~returns false~
All of the logical operators must have zero or non-zero numbers as their operands.
The following truth-tables will show every logical return from each combination of true or false inputs for each logical operator. The left and right columns indicate the truthiness of both the left- and right-hand operands, and the last column indicates the result.
left | right | logical-and (&) -----+-------+---------------- 0 | 0 | 0 0 | 1 | 0 1 | 0 | 0 1 | 1 | 1 left | right | logical-nand (@) -----+-------+---------------- 0 | 0 | 1 0 | 1 | 1 1 | 0 | 1 1 | 1 | 0 left | right | logical-or (|) -----+-------+---------------- 0 | 0 | 0 0 | 1 | 1 1 | 0 | 1 1 | 1 | 1 left | right | logical-nor (\) -----+-------+---------------- 0 | 0 | 1 0 | 1 | 0 1 | 0 | 0 1 | 1 | 0 left | right | logical-xor (#) -----+-------+---------------- 0 | 0 | 0 0 | 1 | 1 1 | 0 | 1 1 | 1 | 0 left | right | logical-xnor (?) -----+-------+---------------- 0 | 0 | 1 0 | 1 | 0 1 | 0 | 0 1 | 1 | 1 right | logical-not (^) -------------+---------------- 0 | 1 1 | 0
All of the above operators and types are vital for building up statements. However, they have one major problem as they've been explained so far. As soon as a statement is evaluated and a result is calculated, that result is immediately thrown away and is gone forever. This would seem to make the whole exercise pointless. However, that's where symbols and the assignment operator come into play.
The assignment operator is used to create symbols. A symbol "points" to a piece of data assigned to it and can be used in place of the data itself. E.g.,
my_symbol: 5;
creates a symbol named my_symbol and assigns the number 5 to it. Once that's done, you can use my_symbol in place of 5.
sum: my_symbol + my_symbol; ~sum is assigned the number 10~
If you assign a new value to an existing symbol, the previous value is deleted and the new value takes its place. E.g.,
value: 12; ~the symbol "value" is created and assigned the number 12~ value: "Cell Runner"; ~The previous value 12 is discarded and the string "Cell Runner" is now what is pointed to by the symbol value~
You can also use a symbol in a statement that results in assigning that same symbol a new value.
counter: 1; ~set counter to 1~ counter: counter + 1; ~add one to counter, and assign the result to counter, making it instead a symbol for the number 2~ counter: counter + 1; ~and again, counter is now a symbol for the number 3~
This operator is mentioned here for completeness of this section, but will be described later when more complex data types are introduced. The member selector operator is the apostrophe (').
When a statement is parsed, its operators are processed in a specific order that is set up to make as much logical sense as possible. When learning arithmetic, for example, we are taught that we always perform multiplication before addition unless a pair of parenthesis are in place to indicate a higher precedence. When programming, you may also use parenthesis to override the default order of operations. E.g.,
5 + 6 * 4; ~returns 29 because 6 * 4 is calculated first~ 4 * 6 + 5; ~reversing the statement has no effect on the calculation~ 4 * (6 + 5); ~The parenthesis force the addition to be done first, yielding 44~ 2 + ((5 + 2) * (18 / 3) - 7); ~Parenthesis can be nested as deeply as desired, just be sure to close all open parentheses before the end of the statement~
The following table lists the order of operations for all the operators provided by the language. Any operators within the same group will be evaluated at the same time as they appear in your statement, working through it either left-to-right or right-to-left as indicated.
1. couples (left to right) {} block [] function call () precedence 2. selector (right to left) ' member selector 3. unary (right to left) ^ not 4. multiplicative (left to right) * multiply / divide % modulo 5. additive (left to right) + add - subtract $ concatenate 6. relational (left to right) < less-than > greater-than <= less-than-or-equal >= greater-than-or-equal 7. equality (left to right) = equal ! not-equal 8. logical AND (left to right) & logical-and @ logical-nand 9. logical OR (left to right) | logical-or \ logical-nor 10. logical XOR (left to right) # logical-xor ? logical-xnor 11. assignment (right to left) : assignment
A block is a way to group multiple statements together and have the language treat the group as if it were a single operation. A block is started with an open brace ({) and ended with a close brace (}). When a block is encountered, all of the statements within it are processed in the normal way except the result of the last statement is returned. E.g.,
result: { first: 5; second: 8; third: 2; first + second + third; };
Here, within the block, three symbols are created and assigned numbers. Then, the three symbols are added together. Since this addition statement is the last one in the block, the result of the addition is returned from the block and therefore the symbol "result" is given that value. Using blocks may seem a bit pointless as this point, but when functions are introduced later, their value will become clear.
White-space (spaces, tabs, returns -- basically any "clear" character) are completely ignored and can be omitted entirely or added as liberally as you want. E.g.,
5*(4+2);
is interpreted identically to
5 * ( 4 + 2 ) ;
The one white-space "gotcha" to consider is when using the minus character (-). This is because the minus character can have two different meanings. It can either be the subtraction operator, or, when placed directly before a number, means that number is is negative.
I tried to design the language so that no characters have more than one meaning. I succeeded as far as I could, but couldn't avoid this double-meaning of the minus character. Oh well.
In the case of minus then, one needs to use white-space to make your intention clear. E.g.,
8-2;
is interpreted as two numbers, 8 and -2, adjacent to each other. Since this doesn't make sense to the interpreter, an error will be generated. To fix this, add spaces.
8 - 2;
Generally speaking, you may style your programs any way that you like, but I recommend following these rules.
I never liked languages that impose structure based on white-space. I feel that this is too opinionated, forcing the programmer to format their code according to the whims of the language designer. I much prefer the use of guidelines such as the above to suggest a style, but ultimately giving complete freedom to the programmer to do as they please.
Congratulations! We've now covered almost all of the syntax of the language.
The heart of the language is the use of the numerous functions included with the language, as well as creating your own. Every language feature except those listed above, are achieved with functions.
Every function, either the built-in ones, or those you create yourself, is represented with a symbol. Calling a function involves passing zero or more arguments into it, and getting a result back from it.
For example, the "substr" function takes either two or three arguments. substr means "sub-string" and extracts part of a string and returns it. The three arguments are (1) the string itself from which you want to get the substring, (2) the starting position for the substring where 0 is the first character, 1 is she second character, and so on (if you use a negative number, it works backward from the end), and optionally (3) the length of the substring (if the third argument is omitted, the remainder of the input string to its end is returned. E.g.,
alphabet: "abcdefghijklmnopqrstuvwxyz"; substr[alphabet, 5, 10]; ~returns the string "fghijklmno"~
Notice that calling a function is done by first writing the function's symbol, then an open bracket, then the arguments to pass into the function each separated by a comma, and finally the close bracket.
In addition to calling any of the numerous built-in functions, you can also create your own. This is done with the "funct" built-in function. funct is literally a function that creates and returns functions that can then be assigned to a symbol. For example, suppose you wanted to create a function that returns the number 5, you could write,
get5: funct[5];
and the symbol get5 now points to a function that returns the number 5. You would call this function like any other, by writing get5 followed by the open and close bracket.
get5[]; ~returns 5~
Since this function takes no arguments, none are needed within the brackets. Of course, this is an incredibly useless function since you could just type a literal 5 in its place.
To make something with more value, we can create a function that takes arguments. This is done by making the first arguments to the funct function a list of symbols, and the last argument to funct the actual operation to perform.
addem: funct[a, b, a + b];
When addem is called, it expects two arguments. These are then assigned to a and b within the context of the operation to perform, a + b. E.g.,
addem[5, 6]; ~returns 11~
After the function call is complete, a and b are discarded and only the return value is given back to where the function is called.
So far, so good. However, it may seem that there is only so much a function can do when the operation given it to perform is only one expression. This is where blocks come in. Since a block takes a group of statements, returns the result of the last statement, and treats the whole block as one statement, this adds tremendous value to what a function can perform. E.g.,
domath: funct[a, b, { c: a + b; d: c + a * 2; a * b / c; }];
This is just a bunch of nonsense math I made up, but hopefully the point is clear. funct creates and returns a function that takes two arguments, which is then assigned to the symbol domath. When called, domath takes the two arguments given to it and performs the statements listed, ultimately returning the calculated value of the last statement to the caller. It should also be noted that the symbols created within the block, as well as the symbols of passed-in arguments are not kept after the function returns. (A detailed description of symbol scope will be described later.)
Note also the indention and formatting style used. Formatting your programs are not required by the language, but using a consistent style helps make your code more readable. Here, by giving the parameters on the same line as the funct, and the actual body of the code below and indented, helps make it clear what is going on.
"Control flow" refers to the ability to change what a program does based on specific conditions. We'll demonstrate this with the "if" function. This function takes at least two arguments. If the first argument evaluates to "true" (that is, non-zero) then the contents of the second argument are evaluated. If the first argument evaluates to "false" (that is, zero) then the second argument is not evaluated.
a: 5; b: 6; c: if[a < b, "less than"]; ~since a is less than b, the second argument is evaluated, and the if function returns the string "less than" and it is assigned to the symbol c~
On the other hand, if we swap the assignments of a and b, the result of the comparison between and and b will be false and the second argument to the if function will not be evaluated.
a: 6; b: 5; c: if[a < b, "less than"]; ~since a is not less that b, the second argument is not evaluated and the if function will return nil instead, which is then assigned to the symbol c~
The if function has more tricks up its sleeve. If you supply a third argument, it is evaluated instead if the first argument is evaluates to false.
c: if[a < b, "less than", "greater than"];
Now you'll get a string returned by if either a is greater than b or if it is less than.