💾 Archived View for gem.sdf.org › rellwood › doc.gmi captured on 2024-05-12 at 15:18:26. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-12-28)
-=-=-=-=-=-=-
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 games.
At its most fundamental, writing code consists of combining five basic types of elements: numbers, strings, symbols, and nil. (There are other more complex 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 .2
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 in the case of 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 some exceptions to this as you'll see later on.)
They can be made up of the Latin letters A - Z, either upper- or lower-case, numbers and the underscore (_), but cannot start with a number. They can be any length and are not 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, like symbols, not case-sensitive.
Human-readable comments, annotations, and any other human-readable 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.~
Comments are discarded by the lexer when your program is run so they have no affect on what the computer does. Their purpose is to annotate your program so that when you (or someone else) reads through your program in the future, you'll be able to clearly recall the details of the intent of the program.
I don't like the terms "coder" or "developer" to refer to programmers. Use whatever terminology you like, but I myself feel that "developer" sounds too watered-down, and "coder" is just plain demeaning. Programming is a mystic alchemy, a noble and intellectual art, and should be referred to as such.
A statement is a complete conceptual set of instructions 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 one statement per line. You can have a statement span multiple lines or have several statements in a single line, if you choose.)
All of the above, numbers, strings, symbols, and nil, are put together into these statements with operators. In other words, 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; ~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 and division first. So, the multiplication (6 * 8 = 48) is evaluated, as well as 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 to the language and adds more types that need to be delt with. Simplicity is the goal.
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 anything ercept numbers. 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,
You might be wondiring about the difference between inclusive-or and exclusive-or. 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; ~the symbol "sum" is assigned the number 10~
If you assign a new value to an existing symbol, the previous value is discarded 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; ~create the symbol "counter" and set its value 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~
Some types of data contain a collection of several datum rather than just one. A good example of this that has already been is a string, which contains a collection of characters
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 a given 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. E.g.,
a + b - c;
consists of "additive" operators and will all be evaluated left-to-right meaning that first the addition is evaluated, then the subtraction, since the additon is left of the subtraction. Or, e.g.,
a: b: 5 + 2;
has the addition evaluated first since additive operators have a higher precedence than the assignment operator. Then "b" is assigned to the result of that additon, and then (moving left since assignments are evaluated right-to-left) the "a" symbol is assigned to the result of the "b" assignment. Assigning a symbol "returns" the result of that assignment, so "a" is given that same value that "b" had been given. All of that to say that both "a" and "b" now have the value 7.
There's no need to memorize this table. I recommend gitting the basic jist of it and then use lots of parenthesis to ensure that your statements are evaluated as you want them to be in your actual programs.
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, in the last line of the block, 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 its intention clear. E.g.,
8-2;
is interpreted as two numbers, 8 and -2, adjacent to each other. Because two adjacent numbers don'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, such as Python. I feel that this is too opinionated, forcing the programmer to format their program 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 programmers to do as they please.
Congratulations! We've now covered almost all of the syntax of the language. All that remains unexplained is the member-selector syntax, which we'll get to when we discuss more complex types.
The majority of your use of the language is that of the numerous functions included with the language, as well as creating and using those you create yourself. 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 built-in "substr" function takes either two or three arguments. substr is an abbreviation of "sub-string" and its job it to extract part of a string and return 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 closing bracket.
In addition to calling any of the many built-in functions, you can also create your own. This is done with the "funct" built-in function. "funct" is the function that creates and returns the function you create 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, in practical terms, this is a useless function since you could just type a literal 5 in its place.
To make something with more value, let's create a function that itself takes arguments. This is done by making a comma-seperated list of said arguments (themselves symbols) to the "funct" function, and the final 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, i.e., a + b. E.g.,
addem[5, 6]; ~returns 11~
After the function call is complete, a and b are discarded since they only live within the context of the function call, 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. The "funct" function 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 three 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" function call, 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 built-in "if" function. This function takes at least two arguments. If the first argument evaluates to "true" (that is, a non-zero number) then the contents of the second argument are evaluated. If the first argument evaluates to "false" (that is, the number 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 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. The "if" function here can be read as "if [first argument] is true, then evaluate [second argument] otherwise evaluate [third argument.]"