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.
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.
script run print('Hello World!')
or an OVERLY complex example:
/script run block_check(x1, y1, z1, x2, y2, z2, block_to_check) -> ( [minx, maxx] = sort([x1, x2]); [miny, maxy] = sort([y1, y2]); [minz, maxz] = sort([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(); [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.
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.
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 are specific to 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.
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
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'.
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.
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
or as a globally available app in singleplayer in the .minecraft/config/carpet/scripts
folder and loaded
as a Scarpet app with the 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)
There are several preprocessing operations applied to the source of your program to clean it up and prepare for execution. Some of them will affect your code as it is reported via stack traces and function definition, and some are applied only on the surface.
- stripping
//
comments (in file mode) - replacing
$
with newlines (in command mode, modifies submitted code) - removing extra semicolons that don't follow
;
use as a binary operator, allowing for lenient use of semicolons - translating
{
intom(
,[
intol(
, and]
and}
into)
No further optimizations are currently applied to your code.
LR1 parser, tokenizer, and several built-in functions are built based on the EvalEx project. EvalEx is a handy expression evaluator for Java, that allows to evaluate simple mathematical and boolean expressions. EvalEx is distributed under MIT licence. For more information, see: EvalEx GitHub repository