11 KiB
Fundamental components of scarpet programming language (towards version 1.7).
Scarpet (a.k.a. Carpet Script, or Script for Carpet) is a programming language designed to provide the ability to write custom programs to run within Minecraft and interact with the world.
The project was initially built based on the EvalEx project, however it now diverged so far away from the original implementation, it would be hard to tell it without this mention. EvalEx is a handy expression evaluator for Java, that allows to evaluate simple mathematical and boolean expressions. For more information, see: EvalEx GitHub repository
This specification is divided into two sections: this one is agnostic to any Minecraft related features and could function on its own, and CarpetExpression for Minecraft specific routines and world manipulation functions.
Synopsis
script run print('Hello World!')
or an OVERLY complex example:
/script run
block_check(x1, y1, z1, x2, y2, z2, block_to_check) ->
(
l(minx, maxx) = sort(l(x1, x2));
l(miny, maxy) = sort(l(y1, y2));
l(minz, maxz) = sort(l(z1, z2));
'Need to compute the size of the area of course';
'Cause this language doesn\'t support comments in the command mode';
xsize = maxx - minx + 1;
ysize = maxy - miny + 1;
zsize = maxz - minz + 1;
total_count = 0;
loop(xsize,
xx = minx + _ ;
loop(ysize,
yy = miny + _ ;
loop(zsize,
zz = minz + _ ;
if ( block(xx,yy,zz) == block_to_check,
total_count += ceil(rand(1))
)
)
)
);
total_count
);
check_area_around_closest_player_for_block(block_to_check) ->
(
closest_player = player();
l(posx, posy, posz) = query(closest_player, 'pos');
total_count = block_check( posx-8,1,posz-8, posx+8,17,posz+8, block_to_check);
print('There is '+total_count+' of '+block_to_check+' around you')
)
/script invoke check_area_around_closest_player_for_block 'diamond_ore'
or simply
/script run print('There is '+for(rect(x,9,z,8,8,8), _ == 'diamond_ore')+' diamond ore around you')
It definitely pays to check what higher level scarpet functions have to offer.
Programs
You can think of an program like a mathematical expression, like "2.4*sin(45)/(2-4)" or "sin(y)>0 & max(z, 3)>3".
Writing a program, is like writing a 2+3, just a bit longer.
Basic language components
Programs consist of constants, like 2, 3.14, pi, or 'foo', operators like +, /, ->, variables which you
can define, like foo or special ones that will be defined for you, like _x, or _ , which I specific to a each
built in function, and functions with name, and arguments in the form of f(a,b,c), where f is the function name,
and a, b, c are the arguments which can be any other expression. And that's all the parts of the language, so all
in all - sounds quite simple.
Code flow
Like any other proper programming language, scarpet needs brackets, basically to identify where stuff begins and
where it ends. In the languages that uses much more complicated constructs, like Java, they tend to use all sort of
them, round ones to indicate function calls, curly to indicate section of code, square to access lists, pointy for
generic types etc... I mean - there is no etc, cause they have exhausted all the bracket options...
Scarpet is different, since it runs everything based on functions (although its not per se a functional
language like lisp) only needs the round brackets for everything, and it is up to the programmer to organize
its code so its readable, as adding more brackets does not have any effect on the performance of the programs
as they are compiled before they are executed. Look at the following example usage of if() function:
if(x<y+6,set(x,8+y,z,'air');plop(x,top('surface',x,z),z,'birch'),sin(query(player(),'yaw'))>0.5,plop(0,0,0,'boulder'),particle('fire',x,y,z))
Would you prefer to read
if( x<y+6,
set(x,8+y,z,'air');
plop(x,top('surface',x,z),z,'birch'),
sin(query(player(),'yaw'))>0.5,
plop(0,0,0,'boulder'),
particle('fire',x,y,z)
)
Or rather:
if
( x<y+6,
(
set(x,8+y,z,'air');
plop(x,top('surface',x,z),z,'birch')
),
// else if
sin(query(player(),'yaw'))>0.5,
(
plop(0,0,0,'boulder')
),
// else
particle('fire',x,y,z)
)
Whichever style you prefer it doesn't matter. It typically depends on the situation and the complexity of the subcomponents. No matter how many whitespaces and extra brackets you add - the code will evaluate to exactly the same expression, and will run exactly the same, so make sure your programs are nice and clean so others don't have problems with them
Functions and scoping
Users can define functions in the form fun(args....) -> expression and they are compiled and saved for further
execution in this, but also subsequent calls of /script command, added to events, etc. Functions can also be
assigned to variables,
passed as arguments, called with call('fun', args...) function, but in most cases you would want to
call them directly by
name, in the form of fun(args...). This means that once defined functions are saved with the world for
further use. For variables, there are two types of them, global - which are shared anywhere in the code,
and those are all which name starts with 'global_', and local variables which is everything else and those
are only visible inside each function. This also means that all the parameters in functions are
passed 'by value', not 'by reference'.
Outer variables
Functions can still 'borrow' variables from the outer scope, by adding them to the function signature wrapped
around built-in function outer. It adds the specified value to the function call stack so they behave exactly
like capturing lambdas in Java, but unlike java captured variables don't need to be final. Scarpet will just
attach their new values at the time of the function definition, even if they change later. Most value will be
copied, but mutable values, like maps or lists, allow to keep the 'state' with the function, allowing them to
have memory and act like objects so to speak. . Check outer(var) for details.
Code delivery, line indicators
Note that this should only apply to pasting your code to execute with commandblock. Scarpet recommends placing
your code in apps (files with .sc extension that can be placed inside "/scripts" folder in the world files and
loaded as a scarpet app with command /script load [app_name]. Scarpet apps loaded from disk should only
contain code, no need to start with "/script run" prefix
The following is the code that could be provided in a foo.sc app file located in world /scripts folder
run_program() -> (
loop( 10,
// looping 10 times
// comments are allowed in scripts located in world files
// since we can tell where that line ends
foo = floor(rand(10));
check_not_zero(foo);
print(_+' - foo: '+foo);
print(' reciprocal: '+ _/foo )
)
);
check_not_zero(foo) -> (
if (foo==0, foo = 1)
)
Which we then call in-game with:
/script load foo /script in foo invoke run_program
However the following code can also be input as a command, or in a command block.
Since the maximum command that can be input to the chat is limited in length, you will be probably inserting your
programs by pasting them to command blocks or reading from world files, however pasting to command blocks will
remove some whitespaces and squish your newlines making the code not readable. If you are pasting a program that
is perfect and will never cause an error, I salute you, but for the most part it is quite likely that your program
might break, either at compile time, when its initially analyzed, or at execute time, when you suddenly attempt to
divide something by zero. In these cases you would want to get a meaningful error message, but for that you would
need to indicate for the compiler where did you put these new lines, since command block would squish them. For that,
place at the beginning of the line to let the compiler know where are you. This makes so that $ is the only
character that is illegal in programs, since it will be replaced with new lines. As far as I know, $ is not
used anywhere inside Minecraft identifiers, so this shouldn't hinder the abilities of your programs.
Consider the following program executed as command block command:
/script run
run_program() -> (
loop( 10,
foo = floor(rand(_));
check_not_zero(foo);
print(_+' - foo: '+foo);
print(' reciprocal: '+ _/foo )
)
);
check_not_zero(foo) -> (
if (foo==0, foo = 1)
)
Lets say that the intention was to check if the bar is zero and prevent division by zero in print, but because
the foo is passed as a variable, it never changes the original foo value. Because of the inevitable division
by zero, we get the following message:
Your math is wrong, Incorrect number format for NaN at pos 98
run_program() -> ( loop( 10, foo = floor(rand(_)); check_not_zero(foo); print(_+' - foo: '+foo);
HERE>> print(' reciprocal: '+ _/foo ) ));check_not_zero(foo) -> ( if (foo==0, foo = 1))
As we can see, we got our problem where the result of the mathematical operation was not a number
(infinity, so not a number), however by pasting our program into the command made it squish the newlines so
while it is clear where the error happened and we still can track the error down, the position of the error (98)
is not very helpful and wouldn't be useful if the program gets significantly longer. To combat this issue we can
precede every line of the script with dollar signs $:
/script run
$run_program() -> (
$ loop( 10,
$ foo = floor(rand(_));
$ check_not_zero(foo);
$ print(_+' - foo: '+foo);
$ print(' reciprocal: '+ _/foo )
$ )
$);
$check_not_zero(foo) -> (
$ if (foo==0, foo = 1)
$)
Then we get the following error message
Your math is wrong, Incorrect number format for NaN at line 7, pos 2
print(_+' - foo: '+foo);
HERE>> print(' reciprocal: '+ _/foo )
)
As we can note not only we get much more concise snippet, but also information about the line number and position, so means its way easier to locate the potential problems problem
Obviously that's not the way we intended this program to work. To get it foo modified via a function call,
we would either return it as a result and assign it to the new variable:
foo = check_not_zero(foo); ... check_not_zero(foo) -> if(foo == 0, 1, foo)
.. or convert it to a global variable, which in this case passing as an argument is not required
global_foo = floor(rand(10)); check_foo_not_zero(); ... check_foo_not_zero() -> if(global_foo == 0, global_foo = 1)