CUAL(6)
๐ CUAL(6) โ the Cuyo Animation Language ๐
Cual โ the Cuyo Animation Language. Yep, that's the whole name, no notes. โจ
Cual is the main language used to describe the animations in cuyo. Strictly speaking it's the stuff between the << and >> brackets in the level description files (xxx.ld).
On the other hand, this man page is aiming at being a complete description of how to write levels for cuyo. But it's still under construction ๐ง. Go peek at the file example.ld to get an idea of how the rest of the level description works โ there's also a bit of example Cual code in example.ld. And of course, all the existing levels are examples, bestie. ๐ธ
โ ๏ธ Real talk: Cual is probably still very buggy ๐ญ. So if strange things happen and you're sure it's not your fault, tell me (cuyo@karimmi.de). I wanna know. ๐
๐ง HOW IT WORKS ๐ง
The level description is organized in sections. There's a global section, and every level has its own section, which is a subsection of the global section. It's common practice to put each level in a separate file, which then basically starts by opening its section and ends by closing it.
A section is defined by name = {contents}. Here name is the name of the new section, and contents holds the definitions that pertain to that section. That's a sequence of definitions of the form name = stuff. And stuff can be:
{contents}as above, or- a single datum, or
- a comma-separated list of data.
Inside such a list, datum * number can be used as a shorthand for datum, ..., datum, i.e. a number-fold repetition of datum. ๐
A datum can be an identifier, a string (enclosed by '"'), a word, or a number. And in place of a number you can drop in <expression>, where expression is an arbitrary expression made up from literal numbers, previously defined numeric data, and the operators +, -, *, / and %. Math class but make it fun. ๐ฅ
Definitions can also depend on versions. See section VERSIONING below.
Apart from definitions, a section can also contain cual definitions (see below). These have to be enclosed in << and >>.
Each blob has its own (main) Cual procedure which does the drawing and the animation stuff. That procedure only depends on the kind of the blob โ meaning it's the same for blobs of the same kind. However, each blob has its own instance of the variables. ๐
In every game step, the procedure of each of the blobs is called once. (There are 12.5 game steps per second.) It has to draw the blob each time, even if nothing has changed. (However, there's an internal routine in cuyo which checks if the same is drawn as in the last step and which then suppresses the drawing. Iconic efficiency, honestly.)
There may be other procedures associated to a kind of blob, which are executed at special events โ for example when a falling blob lands. In contrast to the main procedure, these event handlers are not allowed to draw anything. ๐ซ See section EVENT HANDLERS for a list of the existing events.
The name of the main procedure of a blob (the one which draws the blob) is the name of the kind of the blob. Normally, that name is the word listed after the pics= entry in the .ld file; but if that word contains a dot, only the part before the dot makes up the name. (E.g. with pics=redblob.xpm,greenblob.xpm, the names are "redblob" and "greenblob".) ๐
The name of an event handler procedure is the name of the kind, followed by a dot, followed by the event name. (E.g. "redblob.land" for the landing event of the redblob from above.)
[Explain the default procedures.]
๐ฎ LEVEL DATA ๐ฎ
๐ String valued data
nameโ the name of the level. It shows up in the list of levels and in the level intro. โจdescriptionโ an optional further description of the level in its intro.authorโ the name of the level author(s), for credit at the beginning of a level. ๐ซถ
๐ท๏ธ Identifier valued data
bgpicโ background picture (file name). If it's too small, it's placed at the bottom. Defaults to none.toppicโ appearance of the top border coming down (file name). Defaults to none.explosionpicโ appearance of the explosions (file name). Has a default.
๐ข Number valued data
numexplodeโ the size that a group of blobs has to reach in order to explode. This is only the level-wide default. Each kind can override it. Whether the group does explode is also controlled bybehaviour. See section VARIABLES AND CONSTANTS for details.toptimeโ the time the border takes to come down, counted in number of game steps. Each game step lasts80ms. The default value is50(i.e. one pixel every four seconds).topoverlapโ placement oftoppicrelative to the actual border. More precisely, the number of pixels the lower border of the picture is below the actual border. Defaults to the height of the picture.topstopโ when the border comes down at the end of the level, the number of pixels it should stop before the bottom. Set this to the same value astopoverlapif you want yourtoppicto be completely visible at the end. Defaults to0.chaingrassโ must be0or1. If set to1, chain reactions are necessary to kill the grass. Defaults to0. More precisely,chaingrassonly controls the default forbehaviourfor grass blobs. See section VARIABLES AND CONSTANTS for details.mirrorโ must be0or1. If set to1, the level appears upside-down. ๐ Defaults to0.randomfallposโ must be0or1. If set to1, the initial fall position is randomized horizontally. Defaults to0.neighboursโ determines in which directions the blobs can connect to each other in order to form groups. This is only the level-wide default; each kind can override this. See section VARIABLES AND CONSTANTS for values. Defaults toneighbours_rect.hexflipโ in hex mode, determines whether the even or the odd columns are shifted upwards. By default (hexflip = 0), the odd columns are shifted.1means: shift even columns of player 1;2means: shift even columns of player 2;3means: shift even columns of both players.randomgreysโ the expected time between two randomly appearing greys, in game steps (80ms). Use-1for none at all, which is the default.nogreyprobโ the probability that a grey does not appear. Seegreyprobandcolourprobin section KIND DATA for details. The default is0.aiu_color,aiu_grass,aiu_grey,aiu_two_above,aiu_monochromic_vertical,aiu_heightโ parameters for the AI player's utility function. They default respectively to<10*(number of kinds)>,20,10,<aiu_color/2>,<aiu_color>, and10. See section THE AI UTILITY FUNCTION for details.
๐จ Colour valued data
(A colour is an RGB triple of numbers between 0 and 255.)
bgcolorโ the background colour. Defaults to white.textcolorโ colour of any text. This includes the beginning-of-level information,message()s, and score. Defaults to a certain shade of dark grey.topcolorโ the colour of the top border coming down (where not determined bytoppic). Defaults to a certain shade of light grey.
๐งฉ Other data
startdistโ distribution of blobs at the beginning of the level. It is a list of strings, the format of which is described in the section STARTDIST.pics,greypic,startpic,emptypicโ lists of kinds. These can be either file names referring to the picture to be used, or declarations of kinds that have to be defined later on. The different keywords (e.g.pics,emptypic) define different defaults. In fact, only the first three may be real lists โemptypicis limited to exactly one entry. In these lists, it's advisable to use*whenever possible. Besides being shorter to write, it also speeds up loading of the level and cuts down memory usage. This is because cuyo does some initializations only once for each entry with multiplier.
The intentions of these lists are normal blob kinds, resp. grey blob kind, resp. grass blob kind, resp. nothing-blob. However, the only differences between pics, greypic and startpic are the default values for behaviour, colourprob, goalprob, greyprob, versions and distkey (see there). All of these can also be overridden individually. Also, the default drawing code is different. (The default drawing code for startpic does not draw connections.) ๐
kindโ each kind can have its own section. See KIND DATA below for the entries of that section.
๐ KIND DATA ๐
okay bestie, these are the per-kind data settings. If you define any of these inside a kind, it overrides the level-wide value for that kind. No cap. And if you need the full tea on them, bounce to section LEVEL DATA. ๐
numexplode,neighboursโ defining these in a kind's section overrides the level-wide value for that kind. See section LEVEL DATA for a description of these data.picsโ a list of file names of pictures to use for this kind. The nth entry can later be accessed incualwithfile=n-1. Iconic.colourprobโ the probability that this kind shows up as one of the two steered falling blobs. More precisely, it's a nonnegative integer weight. To get the actual probability, the value gets divided by the sum of thecolourprobs of all kinds. That sum must be positive. Default is1for kinds declared withpics=and0for all other kinds. This probability is also used for+instartdist. For more details see section STARTDIST.goalprobโ this affects the semantics of*instartdistin the same waycolourprobdoes for+. Default is1for kinds declared withstartpic=and0for all other kinds.greyprobโ the probability that this kind appears as a grey blob. It's similar tocolourprob, but there's a difference: forgreyprob,nogreyprobis included in the sum, so it might happen that no blob appears at all. There's a notable difference between a positivenogreyproband a positivegreyprobin kindnothing, when several lines of grey blobs appear: in the latter case, empty blobs appear in the wall of greys, making holes. In the former case, the wall is made less high. Usually this is preferable. Default is1for kinds declared withgreypic=and0for all other kinds. This value also affects the semantics of-instartdist. In this case,nogreyprobis not included in the sum.versionsโ at the creation of a blob, itsversionvariable is initialized. Usually, it's chosen at random from0toversions-1, butstartdistprovides the possibility to specify it exactly. See section STARTDIST for details. Default is1.distkeyโ an alphanumerical key, used instartdistto identify this kind of blob. Default isAfor kinds declared withstartpicand undefined for all other kinds. See section STARTDIST for details.
โจ CUAL DEFINITIONS โจ
Inside << >>, variable and procedure definitions are expected. It's giving very much "fill in the blanks, bestie." ๐ฆ
procname = code ;โ defines a "procedure". The next section describes how code looks like. Example:
redblob = {
schema16; 0*;
1; A,B,C; *;
};
var varname1 [= def1 [: reapply]], varname2 [= def2 [: reapply]], ... ;โ defines variables with default values. If no default is specified, zero is used. See section VARIABLES AND CONSTANTS about the meaning of the default value and the optional suffix: reapply.default varname1 = def1 [: reapply], varname2 = def2 [: reapply], ... ;โ changes the default for already defined variables. Again, the suffix: reapplyis optional. This is useful to give a single kind a different default for a variable than the other kinds. Also, the default of a system variable can be changed this way.
๐ป CODE ๐ป
A code fragment can be one of the following. It's a whole vibe check for your blob logic. ๐ฅ
{ code; code; ...}โ executes one command after the other. Stack 'em up.code, code, ...โ useful for simple animations. Executes exactly one of the commands: in the n-th call, the n-th command is executed. After the last command, the first one is executed again. However, if one of the commands is "busy" (see section BUSIENESS), this one will be executed until it stops being busy, and only after that, the next command will be executed. No cap.procnameโ executes the procedureprocname, which has to be already defined. The result is the same as if the code fromprocnamewould have been inserted in that place.&procnameโ executes the procedureprocname; however, every instance of such aprocnameis the same. This concerns busieness and the state of an animation sequence. (See sections BUSIENESS and AMPERSAND-CALL.)busyโ does nothing except being busy. (See section BUSIENESS.)varname = exprโ sets the variable. See section VARIABLES AND CONSTANTS for details.- The same with
+=,-=,*=,/=,%=,.+=,.-=โ does what you would expect. Slay. [ varname = expr ] codeโ sets the variablevarnametoexpr, executescodeand then resets the variable to the old value. Temporary main character moment.numberโ a shortcut forfile = number.letterโ a shortcut forpos = number, where different letters mean different numbers:A: 0,B: 1, ...,Z: 25,a: 26, ...,z: 51. Alphabet math, periodt.*โ draw the icon specified by the variableskind,fileandpos. May also draw only a part of the icon, if specified by the variablequ(see section VARIABLES AND CONSTANTS).*@(position)โ like*, but draws the icon at some other position. This drawing is performed after all drawing by*. If*@is used from several blobs, the further order of drawing is not specified. It is guaranteed, however, that at any given time this order is the same for all positions. (See section VARIABLES AND CONSTANTS for more details about@.)@(position)*โ like*, but draws the icon at some other position. This drawing is performed before all drawing by*. If@*is used from several blobs, the further order of drawing is not specified. It is guaranteed, however, that at any given time this order is the same for all positions. (See section VARIABLES AND CONSTANTS for more details about@.)if expr if-arrow if-code ;andif expr if-arrow if-code else [else-arrow] else-code ;โ the arrows can be either"->"or"=>". If you use"->"arrows, it does exactly what you would expect. If the if-arrow is"=>", then once the expression gets true, the if-code will be executed every subsequent time (without testing the condition), as long as it is "busy". For more details see section BUSIENESS. If the else-arrow is"=>", then once the expression gets false, the else-code will be executed every subsequent time as long as it is busy. The else-arrow may only be omitted if the if-arrow is"->". Then the else-arrow also is"->". (But this might change in the future.)switch { ... }โ exact form:
switch {
expr1 arrow1 code1 ;
expr2 arrow2 code2 ;
...
}
The arrows can be either "->" or "=>". Does the same as:
if expr1 arrow1 code1
else => if expr2 arrow2 code2
...
The last expr may be omitted. This is equivalent to setting it to true.
bonus(expr)โ the player getsexprbonus points.message(String)โ the string is displayed (blinking) on the screen. To be used together withbonus(...). Example:
bonus(50);
message("You get 50 bonus points");
explodeโ makes the blob explode. For the next 8 steps or so, the blob is still what it was before, but the explosion is drawn over the graphics. After that, it's changed to a nothing-blob. ๐ฅloseโ the players immediately lose the level. Not the vibe.sound(Filename)โ plays the given sound file.
You can also omit the code completely. Then, of course, it does not do anything. This can be useful as part of ,-sequences.
There's a shortcut for drawing: You may omit the ";" between a number, a letter and the "*".
โจ EXPRESSIONS โจ
okay bestie, listen up ๐ the only data type in cual is int. Bools are represented by 0 and 1, like in C. (And any number other than 0 is interpreted as true, if a boolean is expected.) no cap.
Of course, variables, constants and numbers are expressions, and you can use parentheses. There are the following operators (listed here in order of increasing precedence):
||โ Boolean or&&โ Boolean and==,!=,<,>,<=,>=โ Comparison==..โ A special comparison!โ Boolean not+,-โ Add, substract:โ Special operator*,/,%โ Multiply, divide, modulo&,|,.+,.-โ Bitwise and, bitwise or, setting of bits (same as bitwise or), unsetting of bits-โ Unary minus.โ Testing of bits (a.bis the same asa&b != 0)
/ and % work mathematically correct and do not make funny changes when the sign of the numerator changes. More specifically, if b is positive, then a/b is the largest integer n such that n*b<=a. If b is negative, then a/b is the largest integer n such that n*b>=a. In both cases a%b is such that (a/b)*b+a%b = a. Examples:
13/5=2 13%5=3
-13/5=-3 -13%5=2
13/-5=-3 13%-5=-2
-13/-5=2 -13%-5=-3
โจ The special operators โจ
expr1 == expr2 .. expr3 : Is true, if expr1 lies between expr2 and expr3. You may also omit one of expr2 and expr3. (Then, it does the same as <= resp. >=.) The precedence implies that x==y==2..3 is the same as x==(y==2..3) and is neither (x==y)==2..3 nor x==(y==2)..3. Note that this operator might change in the future. (I plan to make something like "expr in set" in Pascal.)
expr1 : expr2 : Is true (that is, 1) with probability expr1/expr2
neighbour_pattern : neighbour_pattern is a sequence of six or eight characters 0, 1 and ?. It is true if the sequence fits to the neighbour sequence of the blob. The neighbour sequence is a string of "0"s and "1"s with a "1" for each neighbour of the same kind, starting above and going clockwise. This way, you get a string of "0"s and "1"s (six or eight, depending on wether this level is in hex mode).
Example: 1???0??? is true iff the blob above this blob is of the same kind and the blob below it is of different kind.
For an empty blob the semantics is slightly different: If in some direction there is no neighbour, because the field ends there, the entry in the neighbour sequence is 1 nevertheless. So for an empty blob 1???0??? is true, iff the blob above this blob does not exist or is empty as well, and the blob below this blob exists and is not empty.
If some blob changes its kind during a step, the expression will still test the neighbours as they were at the beginning of the step. (See the section VARIABLES AND CONSTANTS for details.)
๐ The functions ๐
rnd(expr) : Returns a random value between 0 and expr-1
gcd(expr1, expr2) : Returns the greatest common divisor of expr1 and expr2
๐ธ VARIABLES AND CONSTANTS ๐ธ
The following kinds of variables and constants exist:
- User defined variables (see section CUAL DEFINITIONS). At the start of the level (or at the creation of the blob) the value is the default value you provided. If you supplied the default with
: reapply, whenever a blob's kind changes, the value of the variable is also set to the default of the new kind. There is a subtlety: This only happens if the new value of kind is in fact different from the old one. - System variables. These variables are always defined and have special meanings, e.g.
fileandpos. Some of them are read-only. - User defined constants. These are defined in the main
.ldpart, not in cual (not inside<< >>). - System constants. Some of them depend on properties of the level, some are really constant.
Of each variable, there's one instance in each blob. Normally, you access the instance in your own blob, but with the following syntax, you can access variables of other blops:
varname@@(x, y; side)
varname@@(x; side)
varname@@(; side)
varname@(dx, dy; side)
varname@(dx; side)
varname@()
If x and y are given, these are absolute coordinates in the grid of blops, that is the variable is taken from the blob with loc_x=x and loc_y=y (see under The system variables). If only x is given, it specifies one of the two blobs that are currently falling. If there is only one such blob left, because the other one got stuck on some tower, the remaining blob's coordinate is 0. Otherwise one of the two has coordinate 0, the other 1.
In the @ variants, the coordinates are relative to the current blob. The variant @@(; side) refers to the semiglobal blob, the variant @() to the global blob (See section THE GLOBAL BLOB). The extra part ; side is optional and specifies the side of the game. This is only meaningful in two-player mode. side = < specifies the left player, side = > the right player, side = = the player to which the current blob pertains, and side = ! the other player. @() and @@() can also be given as @ respectively @@.
This can be done for both, reading and writing variables. It also works for system variables (but not for constants).
In hex mode levels, for odd dx, dy should be a "half integer", that is a number ending in .5. This is the only place in Cual where non-integers appear. Especially, .5 is not allowed in composite expressions. Therefore, also integer dy is always allowed. If a half-integer is expected and an integer is given, it is assumed to be rounded to above, that is 5 then represents 4.5 and -5 represents -5.5.
Caution: With mirror=1 the absolute and the relative coordinates use different coordinate systems. Handle with extreme care.
Accessing foreign variables is not as easy as it might look at first glance; it might easily introduce a dependence of the internal order of execution of the blob codes. For this reason,
- reading variables with
@or@@always returns the value of the variable it had at the beginning of the current step, that is, before any of the blob codes has been executed. - when writing variables with
@or@@, the write operation will only be executed at the end of the current step. (The write operations are stored in a kind of queue.)
This is also true if a blob accesses its own variables with @(0,0).
The operators +=, -=, etc. are also performed in the future if the left hand side is an @-variable. (To be more precice, the right hand side is calculated instantanousely.)
For illustration, look at the following six statements:
1) X += 1
2) X@(0, 0) += 1
3) X = X + 1
4) X = X@(0, 0) + 1
5) X@(0, 0) = X + 1
6) X@(0, 0) = X@(0, 0) + 1
Only 1) and 3) do the same; they simply increment X by 1. Statement 4) sets X to one more than it was at the beginning of the step. Statements 2), 5) and 6) cause the value of X to be changed in the future (after the current step): X is set to one more than:
2)the value ofXjust before the change (that is,Xis incremented in the future),5)the current value ofX,6)the value ofXat the beginning of the step.
๐ Some more details ๐
- Whenever you try to access a variable at a location which doesn't exist, you will get the default value. If default values depend on the kind, the default pertaining to the blob executing the code is used. This may change in the future.
- Changing a variable which doesn't exist does nothing (and does not result in an error).
[Add explanation of time slices; roughly: @-access of variables in reality don't access the value at the beginning/end of the game step, but of the time slice. The call of the main procedure of all blobs happens in the same time slice, but each other kind of event has its own time slice.]
๐ The system variables ๐
fileโ Specifies the file number from which to take the icon that is drawn by"*". This variable is reset to0before the drawing procedure is executed.posโ Specifies the position in the file of the icon that is drawn by"*". This variable is reset to0before the drawing procedure is executed.kindโ The kind of the blob. There are constants for the possible values of this variable. If you change the kind, you should be aware of three things:- Expressions like
"001???01"test the neighbour pattern at the beginning of the current step. So the change of the variablekindwill not be reflected. - In the current step, the program to draw the blob has already been invoked (it might even be the program which changed this variable); so in this step, the blob will still look like one of the old kind. However, if things are drawn after the kind has been changed, icons from the new kind are taken.
- Defaults of the new kind that are declared with
: reapplyare applied. This happens at the same time that kind changed, but only if the new kind is different from the old one.
- Expressions like
versionโ Is assigned a hopefully distinctive value at the blob's creation. See versions in section KIND DATA for details.quโ Tells"*"which part of the icon to draw. It's possible to draw the whole icon, or only one of its quarters. If a quarter is drawn, you may specify independently which of the quarters to take and at which position to draw it. Use the constants (see below). This variable is reset to"draw all"before the drawing procedure is executed.out1,out2โ Set these Variables for debug output. The values will be printed on top of the blob. These variable are reset to"output nothing"before the drawing procedure is executed. (In fact,"output nothing"is one special big value.)weightโ Whencalculate_sizeis set in behaviour, size will be regularly updated to the sum ofweightin the connected component. The default is1.inhibitโ Set this variable to a sum of the constantsDIR_...; this will inhibit that this blob connects into the given directions. This is not for the graphics but for the calculation of the connected components and the explosions.behaviourโ bestie this one is a bit field, like a little stack of switches all squished into one number ๐ . Peep The Constants down below for what each bit actually means. The defaults are giving:calculate_size+explodes_on_sizefor normal blobsexplodes_on_explosion+explodes_on_chainreactionfor grey blobsfloatsfor the empty blobgoalblob+explodes_on_explosion+explodes_on_chainreactionorgoalblob+explodes_on_chainreactionfor grass blobs โ which one depends on whetherchaingrassis set โจ
falling_speed,falling_fast_speedโ ok so these two are only used in the semiglobal blobs, no cap ๐ซถ. They set the vertical speed of the steered falling blobs. Unit is pixels per game step. Defaults are6and32.
๐ธ The system read-only variables ๐ธ
timeโ the number of time steps since the level was started โฑ๏ธturnโ it's1resp.2if the blob is falling and just being turned by the user, and0otherwise. (1in the first turning step,2in the second one.) Lowkey important: if the user presses the turn key fast several times, some of these steps may be omitted ๐. (Use theturnevent if you want to be sure that a program block is executed once for every turn.)connectโ contains internal data. Will be removed. Probably. ๐sizeโ the size of the component of the blob. (That is, how many blobs are connected.)basekindโ the value of the constant generated for the name of the kind of the blop. Example:
pics = orange, pear, apple * 3, banana, apple
Here, all four kinds apple have the same value for basekind, and this value is apple ๐
loc_x,loc_yโ the absolute coordinates of the blob.(0,0)= top left cornerloc_xx,loc_yyโ the absolute coordinates of the blob in pixels. This is not always the same asloc_x*32andloc_y*32, particularly for the steered falling blobs ๐loc_pโ the player of the blob (1or2)fallingโtrueif the blob is falling or it is a preview of a falling blob. (Falling in the sense of steered by the player. Grey blobs are not falling in that sense.) ๐ฆfalling_fastโtrueif the blob is falling fast, that is, the user pressed the down key โฌ๏ธinformationalโtrueif the blob is one of the info-blobs at the side of the game area. In this sense, the previews of the falling blobs also count as info-blobs.playersโ the number of players.explodingโ when the blob is exploding, this is the position in the explosion animation (1to8);0else ๐ฅ
Currently, there is one exception: if the explosion has been triggered by the explode command, then exploding will have value 1 only after the current game step [more precisely: time slice; fix that]. Reason: when reading exploding@(x,y), we maybe don't know yet that the other blob calls explode. ๐
๐ The Constants: ๐
โจ Constants for behaviour: โจ
goalblobโ setgoalblobif this blob should act like grass: you will have to get rid of it to win the level and making this blob explode will give more points ๐ฅcalculate_sizeโ when this bit is set,sizewill be regularly updated to the sum ofweightin the connected component.explodes_on_sizeโ when this bit is set, a connected component explodes when it hassize>=numexplode๐explodes_on_explosion,explodes_on_chainreactionโ when these bits are set, the blob explodes whenever an explosion that was triggered byexplodes_on_sizehappens in its neighbourhood.explodes_on_chainreactionrefers to those triggering explosions that are the second or later part of a chain reaction.explodes_on_explosionrefers to the other ones.floatsโ when this bit is set, the blob keeps its vertical position even if there is an empty blob below. This bit has no effect on the steered falling blobs ๐
๐ Constants for kind: ๐
<name of kind of blob>โ for each kind of blob, there's one constant with the name of that kind. Use it to check if a blob is of that kind usingkind@(x,y) == aKind, or to change to that kind usingkind = aKind. Seekindunder The system variables for the side-effects of settingkind.
Sometimes it is necessary to perform arithmetic on kinds, for example when several have been declared using the * multiplier. The values of the constants are successive in the order in which the kinds have been declared. When a name is used several times, the first use defines the value. Example:
startpic = apple, orange
pics = orange, pear, apple * 3, banana
greypic = pineapple
This initializes 2 kinds with the defaults for startpic, 6 kinds with the defaults for pics, and 1 kind with the defaults for greypic. The value of the constant orange is 1 more than that of apple, pear is 2 more than orange, banana is 4 more than pear and pineapple is 1 more than banana. We do not specify what these values actually are ๐คซ
This constant also exists for the empty kind, if one has been declared using emptypic. In this case the value's relation to the other values is not specified at all.
global,semiglobalโ denote the kind of the global, respectively semiglobal, blob.nothingโ is the same as the constant for the empty kind. Is provided, because sometimes you don't have an empty kind, but you still need to test if a blob is empty ๐ซฅoutsideโ the value ofkindif the coordinates are outside of the game board.
๐ง Constants for neighbours: ๐ง
neighbours_rectโ a blob connects up, down, left, and right. This is the default.neighbours_horizontalโ a blob connects left and right.neighbours_verticalโ a blob connects up and down.neighbours_diagonalโ a blob connects diagonally.neighbours_hex6โ when used in the level-wideneighbours, this sets hex mode. A blob connects up, down, left with a slight upwards shift, left with a slight downwards shift, right with a slight upwards shift, and right with a slight downwards shift ๐neighbours_hex4โ when used in the level-wideneighbours, this sets hex mode. A blob connects left with a slight upwards shift, left with a slight downwards shift, right with a slight upwards shift, and right with a slight downwards shift.neighbours_knightโ a blob connects in knight moves (two forward and then one sideways. Forward is one of up, down, left or right. Sideways is perpendicular to forward. This makes a total of eight directions.) โneighbours_eightโ combinesneighbours_rectwithneighbours_diagonal.neighbours_3Dโ a more obscure mode created especially for3d.ld. When used in the level-wideneighbours, this sets hex mode. A blob connects up, down, two (but not one) to the left, and two to the right. In even columns it also connects right with a slight upwards shift. In odd columns it also connects left with a slight downwards shift ๐ฝneighbours_noneโ a blob does not connect at all. periodt.
๐ Constants for qu: ๐
Q_ALLโ value forqu, which means "draw the complete picture".Q_TL,Q_TR,Q_BL,Q_BRโ values forqu. "TL" means draw top-left quarter, etc. (See the"*"command in the Code section.)Q_SRC_DSTโ SRC and DST may beTL,TR,BL,BR. Take quarter SRC and draw it at position DST.
โญ Miscellanious constants: โญ
DIR_XXโ to be used with the variableinhibitto prevent the blob connecting in the given directions.XXcan beU,D,L,R(horizontal and vertical);UL,UR,DL,DR(diagonal);UUL,UUR,DDL,DDR,LLU,LLD,RRU,RRD(knight);F,B(3d) ๐งญ
โจ VERSIONING โจ
Okay so sometimes you need one level to behave a little differently depending on what it's being used for. Like, maybe you gotta drop numexplode for the two-player version so it doesn't become completely unplayable ๐ญ. The difficulty settings from cuyo's main menu? Same vibe, another example.
You do this by qualifying a definition with the versions it should apply to. Honestly the best way to get it is just to look at an example:
numexplode = 8
numexplode[2] = 6
numexplode[1,hard] = 10
So this is saying: normally numexplode is 8. In two-player mode it's 6. In one-player hard mode it's 10. The specifiers here are 2 for two-player mode, 1 for one-player mode, and hard for hard mode. Along with easy for easy mode, these are all predefined specifiers that are intended for levels. And then (for internal purposes, not for you bestie) there are also specifiers for the level tracks: main, all, game, extreme, nofx, weird, and contrib.
And here's the fun part: you can make up and use your own specifiers ๐. For them to actually take effect though, you have to give cuyo extra info about the current version. That happens on the command line with the --version option. Like:
cuyo --version=hard,geek
This specifies both the hard version (which, yes, you can change in the menu) and the version geek, which is not predefined. You invented it, you named it, you slay ๐
.
Now there are several constraints you gotta observe, and no, you can't skip them:
- All versions of a definition must be made before the first use of the thing defined. And since it's sometimes not obvious where the defined thing is used (for example,
startpicuses a previousgreypicby assigning successive numbers to kinds), it's best to just group all these versions into one block. Trust. - A given version also applies to every more specialized version for which no definition is given. In the example above,
numexplodeis set to6in two-player hard mode and in two-player easy mode as well. All resulting conflicts must be resolved. For example, if you make a definition for[2]and one for[hard], you must also make a definition for[2,hard](or, equivalently, for[hard,2]), because otherwise it'd be ambiguous which of the two former ones should apply in two-player hard mode. Ambiguity is not the vibe ๐. - Cuyo knows that
easyandhardexclude each other. So it's unnecessary (and actually prohibited) to give an[easy,hard]definition, even if both[easy]and[hard]are given. Same energy for1and2, and for level tracks. - And cuyo also knows that
1and2are exhaustive: there's no mode that's neither single-player nor two-player. (The human-versus-AI mode counts as two-player as far as cual is concerned.) So if there are definitions for both, it's unnecessary โ and again illegal โ to also define a version without any of the two. For example, if[1,de]and[2,de]are given,[de]must be omitted. Or alternatively, you could of course give[1,de]and[de], or[de]and[de,2]. The level track specifiers are exhaustive as well.
๐ธ BUSIENESS ๐ธ
(No, not Business ;-)
Okay so Busieness is a concept that makes it easier to implement simple animated sequences that get triggered by certain events โจ. Each code fragment has an internal state that tells you whether it's busy or not.
- Normal statements like assignments are never busy.
- A chain of commands separated by
,is busy as long as not all of the commands have been executed. code1 ; code2is busy as long as at least one ofcode1andcode2are busy.
Here's an example of how to use busieness for an animation that shows up at random intervals ๐ฆ:
switch {
1:100 => {B*, C*, D*, E*};
-> A*;
};
So this code fragment normally draws the icon at position A (0). But in each step, with a probability of 1/100, an animation sequence consisting of icons B, C, D and E gets started. With a normal arrow (->) after the 1:100, then after the step in which B has been drawn, the probability would be 99/100 that A is drawn again. But with the double arrow, the switch statement won't switch back to A until the animation has terminated. The way that works? Iconic. ๐
(Btw: it doesn't matter if there's a -> or a => before the A*; A* isn't busy anyway.)
๐ THE GLOBAL BLOB ๐
Apart from the normal blobs you can actually see on screen, there's one global blob โ one for the whole game, not one per player. And like, it isn't really a blob, but it behaves a bit like one. It has its own set of variables, and it can have a program that's run once every step โญ.
To define such a global program, use global=code. But here's the thing: the global variables exist even if you don't define global code. See the section VARIABLES AND CONSTANTS on how to access them. Also note that the global blob is always executed before any of the normal blobs. Main character energy, honestly โ it goes first. ๐
There are also semiglobal blobs. There's one for each player. These are programmed with semiglobal=code.
๐ EVENT HANDLERS ๐
The following events exist, bestie, and they're all kinda important:
initโ Is called only once, when the blob gets into life, just before the first time its main drawing routine is called.turnโ Is called for falling blobs each time they are rotated.landโ Is called when the steered blob lands (just after it landed).changesideโ Is called when a blob moves from one player to the other, just after the blob has arrived at the new player.connectโ Is called when the connection of blobs is recalculated.row_upโ Is called when a player got a row from the other side, after everything is finished and just after theloc_yof all blobs has been decreased by 1. Is only called for the semiglobal blob, though.row_downโ Is called when a player gives a row to the other side, before anything visible happens, but just after theloc_yof all blobs has been increased by 1. Is only called for the semiglobal blob, though.keyleft,keyright,keyturn,keyfallโ Are called when the player presses the left, right, turn or fall key. Are only called for the steered falling blobs and the semiglobal blob, though. (Note that, in contrast toturn,keyturnis called even if the steered blob cannot be rotated due to some obstacles, and also if there is no steered blob. Yes, even then. ๐)
๐ฆ THE LIFE OF A BLOB ๐ฆ
Normal blobs come into life at the beginning of the game, or they fall into life: either as colored blobs, steered by the user, or as grey blobs. More precisely, the steered blobs already come into life when they appear as the preview ๐.
When a blob moves (by gravity or when rows go from one player to another), it takes its variables with it. And when a blob explodes, it does not stop existing โ rather, it transforms into an empty blob. That's important for the variables: the empty blob still has all the variables set to the values they had before; only its kind is different. Empty blobs are everywhere where there's no other blob. (However, the falling blobs steered by the user are in some sense "above" everything else; there are empty blobs beneath them. Little stacked situation. ๐ )
The life of empty blobs is different from the one of normal blobs. Empty blobs are not affected by gravity, and they often start or stop existing ๐ธ. For example, when a single grey blob is falling down, the empty blob below it stops existing when the grey blob arrives, and a new empty blob starts existing when the grey blob moves on. There's only one situation in which empty blobs move: when a row moves from one player to the other, and everything moves up resp. down, the empty blobs move, too.
๐ STARTDIST ๐
okay bestie, this field is a whole situation but the good news is that the complicated-ness means you can do a TON with barely any effort, which is iconic behavior honestly. so we're gonna do the single-character format first, because at the time of this writing that's all anyone has ever needed, and then we'll get into the general format as an extension. buckle up ๐
every line of startdist describes one row of blobs in the level's initial state. the lines are aligned to the bottom, and the topmost lines come first (normal reading order, we're not animals). each line must contain exactly 10 or exactly 20 characters โ except the last line, which is special and we'll get to her. in a line of length 20, the first 10 characters describe the left player and the second 10 characters describe the right player. a line of length 10 describes both players. so yeah: each character = one blob. the semantics:
.โ an empty blop. nothing to see here, literally.+,-,*โ a blop chosen at random, according tocolourprob,greyprob, andgoalprobrespectively. the value ofnogreyprobhas no influence here, so don't come for her.0..9,A..Z,a..zโ these denote a specific kind. if the character matches thedistkeyof some kinds, the first of these is chosen. more generally, these characters are ordered such that"9"comes before"A"and"Z"comes before"a". in this order, the maximaldistkeywhich does not come after the character specifies the blob's kind. the difference between the character and thedistkeythen specifies the blob's version.- Example 1: in the special case where the character exactly matches a
distkey, version is set to0. slay. - Example 2: suppose kind
applehasdistkey = "A", kindorangehasdistkey = "O"and no further distkeys exist. then the character"C"denotes an apple withversion=2, the character"N"denotes an apple withversion=13, the character"O"denotes an orange withversion=0, the character"S"denotes an orange withversion=4, the character"a"denotes an orange withversion=12, and the character"8"does not denote anything (and hence is illegal ๐).
- Example 1: in the special case where the character exactly matches a
%โ an info blop with the version set according to the level-wideneighbours.&โ an info blop with the version set according to the level-widechaingrass.
so about that last line ๐ she may have length 4, 8, 10, or 20. if she's length 10 or 20, she's just a normal line like all the others, no drama. otherwise she describes the informational blops next to the field:
- in case of length 4, the first entry describes the blop which depicts the number of greys. the second entry describes the blop which depicts the number of grass blops. the third entry describes the blop which depicts connection information. the fourth entry describes the blop whoch depicts chaingrass information.
- in case of length 8, the above holds for the left player. the remaining 4 entries then describe the same for the right player, but in reversed order. yes really.
the default is "-*%&&%*-" (or equivalently "-*%&"). periodt โจ
as seen above, startdist can reference 62 kind/version combinations directly (and more at random). because this might at some time not be enough, the multichar extension has been introduced. in this case, each blob is described by more than one character. however, the number of characters per blob must be the same for all keys. hence, the lengths of startdist lines then must be this number multiplied by 10 or by 20. every multicharacter combination starting with ".", "+", "-", or "*" is treated as the corresponding character in single-character format. all other character combinations are treated as numbers in base 62 representation. here, "A" to "Z" are digits with decimal value 10 to 35, and "a" to "z" are digits with decimal value 36 to 61. leading spaces are allowed instead of zeroes (however, the all-space string is forbidden ๐ซ). the maximal distkey which (as a number) is not larger than the number given in startdist, specifies the blob's kind. the difference between the startdist number and the distkey then specifies the blob's version. in the case of multichar distkeys, the default for distkeys of kinds declared by startpic= is 10 in decimal.
for blops whose kinds are chosen at random (i.e. characters "+", "-", "*" in single-character startdists), cuyo tries to make these as different as possible. that means, by a certain heuristic, cuyo minimizes the number of neighbouring blobs of the same kind. "Neighbouring", of course, refers to the neighbours entry. inhibit and the calculate_size bit of behaviour have no effect (these are mutable during the lifetime of blobs, while at the time of startdist processing, no blob's lifetime has started yet). so the only way to influence the unneighbouring (if you really wish to do so), is by setting neighbours appropriately. (of course, this possibility is even more limited, when you intend to set the calculate_size bit during the blob's lifetime.) iykyk ๐ฆ
๐ WHERE DO I PUT THE CUAL CODE? ๐
cual procedures and variables can be defined in different sections of the .ld files, and the vibe check goes like this:
- outside of everything; that code is accessible from every level coming after that definition.
- in the section of a level.
- in the section of a kind.
this basically does what you expect ๐ . however, there's one thing you might want to know: even if you define a variable inside a kind, every blob in that level will have that variable. the only effect of defining the variable in the section of a kind is that this kind is the only one which can access it. sneaky. love that for us.
๐ AMPERSAND-CALL ๐
to explain a bit what calling a procecure with an & means, here two examples. these are the moment bestie, watch closely:
Example 1:
myblob = {
...
switch {
myvar -> { 0A*; 1; A,B,C,D; *; 2A*};
-> { 0B*; 1; A,B,C,D; *; 2B*};
};
};
Example 2:
anim = {1; A,B,C,D; *};
myblob = {
...
switch {
myvar -> { 0A*; &anim; 2A*};
-> { 0B*; &anim; 2B*};
};
};
the difference between these examples is what happens when myvar changes. in example 1, the animation "A, B, C, D" will restart at the beginning (because the two animations are different ones); in example 2, the "same" animation is used in both cases, so the animation will simply continue. (removing the ampersands from example 2 will turn the behaviour to the one of example 1.) the way that one little & changes the whole timeline is giving main character energy fr fr ๐ฅ
๐ THE AI UTILITY FUNCTION ๐
when deciding how to place the steered falling blobs, the AI player tries to maximize a certain utility function. its value is the sum of the values for both steered falling blobs plus aiu_monochromic_vertical in case both blobs have the same color and they get placed vertically. the value for a single blob is the sum of the following:
- for each new neighbour of blob, the neighbour utility.
aiu_two_above, if the blob is two above a blob of same kind.aiu_height*20/(the blob's y coordinate).
the neighbour utility for a single new blob and a single one of its new neighbours is the sum of the following:
aiu_color, if the neighbour has the same kind as the blob.aiu_grass, if the neighbour fulfillsbehaviour.goalblob.aiu_grey, if the neighbour fulfillsbehaviour.explodes_on_explosion.
math girlies rise ๐ง
โญ SEE ALSO โญ
๐ BUGS ๐
probably a lot. the following are just a few known ones:
there are several problems with busieness and that stuff. there are several situations in which Cual doesn't behave in the way I would like, and in other situations I don't know how Cual should behave. no cap, that's the whole bug report ๐ญ
and that's a wrap on this whole page bestie ๐ซถ this one's from the 2014-10-25 era of CUAL(6) โ oldie but a goodie, we stan a classic ๐โจ๐ธ
๐ฆ where this page comes from
- debian bookworm: cuyo-data 2.1.0-1
- debian trixie: cuyo-data 2.1.0-1
- nixos 26.05: cuyo 2.1.0
- nixpkgs unstable: cuyo 2.1.0
- ubuntu jammy: cuyo-data 2.1.0-1
- ubuntu noble: cuyo-data 2.1.0-1build1
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms