Rule file syntax and semantics: what's new in GS5

Updated for ver. 5.001. 2022-05-11

New features

The syntax and semantics of rules have been significantly expanded in Game Server 5.*, in order to allow describing more interesting rules. This section describes major changes, as compared to the baseline description (Syntax).

More variables

The original GS1 included 5 variables:

Additionally, order names (e.g. T, B) evaluated to sets of board positions (depending on the current state of the board).

In GS3, when image-and-property-based objects were introduced, the pc and ps were generalized to

In GS5, the set of variables has been further expanded. This includes:

Can use expressions in all fields of an atom (except for pos:

In GS3 (and GS4), complicated expressions could only be used in the bucket: field of a rule atom. (See Bucket expression arithmetic). For other fields, such as color: or shape:, only constants could be used.

In GS5, expressions, with essentially the same syntax, can be used in all fields of the rule atom, other than the count: field and (for now) ther pos: field. Thus, for example, the atom

      (color:last.color, bucket:last.bucket)
    
will allow the player to pick a game piece of the same color as the most recent removed piece, and to put it into the same bucket.

The new code: field

A new field, code:, can now be used in rule atoms. As all other fields, it is optional. If it is present, it needs to evaluate to a true value (i.e. not to an empty set) in order for the move to be allowed.

For example, the atom

      (code:this.pos==last.pos+1)
    
will allow the player to pick a game piece that is located in the "next cell" to the cell from which the most recently moved piece was picked.

Expanded expression syntax

Operators

In addition to the arithmetic operators described in (See Bucket expression arithmetic), the Rule Game expression syntaxt now also supports comparison operators (==, <, >, <=, >=, !=). They return [1] on success and [] on failure.

Data types

In the original GS1, one could say that the only data type used in the color, shape fields was string constants and sets of strings, while the only data type used in the position and bucket fields was integer (and sets of integer). As variables and operators have been added in GS3 and now GS5, one can now talk about the constants and values of expressions belonging to the following types:
Type Example Notes
String red, "red" For compatibility with GS1-GS4, string constants can be written without quotes in simple expressions in property fields, e.g. color:red or color:[red,green]. Elsewhere, the use of double quotes is strongly recommended, to avoid confusion with variables.
Integer 1, 3, pc, last.color.bucket
Integer ranges [0..30] Only used in property fields, for integer-valued properties. Both bounds of the range must be constants.
Sets [1, 3], [red, green] ["red", last.color] !pc, [] (1) Non-empty sets and empty sets are GS's equivalents of "true" and "false". (2) Order variables (T, B, L1, ...) evaluate of sets as well.
Objects this, last, q.color, q.bucket One can access their fields (e.g. last.shape), or use them in a negation operator (!last)

In reality, all of GS arithmetic is set-based, so a single string or a single number is interpreted as a set of 1 element.

The language used in GS rules is not strongly typed. As in Perl, implicit conversion between integers and strings take places when appropriate and possible. If the expression contains an "impossible" operation, such as division by zero or an attempt to apply an arithmetic procedure to a string, the result will be an empty set.

The equality / inequality operators (==, !==) can be applied both to numbers and to strings. The comparison operators (<, >, <=, >=) should only be used with numbers. Otherwise, an empty set will be returned.

Examples

Example 1: assembling a picture in a specific order

Experiment plan: vm/jigsaw-02

Thus, for example, in Charles' last example (assembling a picture in required order), one can assign to each of the four pieces a numerically-valued property "seq", with values {0,1,2,3}. The property file (shapes/vm/jigsaw-02) is as follows:

image,name,seq
b1-nw.svg,b1-nw,0
b1-ne.svg,b1-ne,1
b1-se.svg,b1-se,2
b1-sw.svg,b1-sw,3
The rule set then can look as follows:
1  ()
(seq:(last.seq+1)%4,  bucket:p)     (seq:(last.seq+3)%4,  bucket:p)
      

Here, the first rule line of the rule set will allow to pick any piece and to put it into any bucket; the control then goes to the second line. The second line will allow to pick a piece whose property "seq" has a value that's subsequent to the one of the last-used piece (in the first atom, clockwise, i.e. 3 after 2, 0 after 3, etc; in the second atom, counterclockwise, i.e. 1 after 2, etc), and to put it into the same bucket as the previous piece.

As with the bucket arithmetic, the arithmetic for the property fields will be set-based, yielding empty set when "last" is not defined, or when a particular property in the last piece wasn't defined.

Example 2

Experiment plan: vm/jigsaw-03

If there are several "pictures" on the board, and the player is allowed to choose any picture to start assembling, but then must assemble the entire picture before proceeding to the next one, we can use the property "body" to identify the picture. Say, here there are 2 pictures (numbered body=0 thru body=1), within each of which the pieces are numbered with seq=0 thru 3. The pieces of each picture must be assembled in different buckets (say, the player chooses the bucket for the first picture at will; the pieces of the second picture being put to the next [clockwise] bucket after the one used for the first picture, and so on).

The property file may look like this:

      
image,name,body,seq
b1-nw.svg,b1-nw,1,0
b1-ne.svg,b1-ne,1,1
b1-sw.svg,b1-sw,1,2
b1-se.svg,b1-se,1,3
b2-nw.svg,b2-nw,2,0
b2-ne.svg,b2-ne,2,1
b2-sw.svg,b2-sw,2,2
b2-se.svg,b2-se,2,3

The rules:

1 (bucket:[  !p*[0,1,2,3],  p+1]  )
(body:last.body, seq:[(last.seq+1)%4, (last.seq+3)%4], bucket:p)

One can also write an equivalent rule set differently, for example like this:

1 (code:!p) (bucket:p+1)
(body:last.body, seq:(last.seq+1)%4, bucket:p) (body:last.body, seq:(last.seq+3)%4],  bucket:p) 

Example 3

What if we have several pictures on the board, and we allow the player to work on them in parallel, with a different destination bucket for the pieces of each picture? Let's first design a game where the destination bucket for the pieces of each picture is predetermined by the designer. Thus, the pieces of the picture with body=0 go to bucket No. 0, etc. Experiment plan: vm/jigsaw-04

(code:!q.body, bucket:this.body)  (seq:(q.body.seq+[1,3])%4, bucket:this.body)

(The first atom allows picking pieces for which q.body.seq is not defined, i.e. pieces of pictures that have not been worked on yet; the other atom allows assembly clockwsise and counterclockwise).

Example 4

What if, as in the previous example, we want the player to "assemble" each picture in a different bucket, allow him to work on them all in parallel, and we want to allow the player to independently choose the destination bucket for each picture? (Not tested)

(seq:(!q.body)*[0,1,2,3], bucket:(!q.bucket)*this.bucket)   (seq:(q.body.seq+[1,3])%4, bucket:p.body)

Here, the first atom allows picking a piece from a "not-started-yet" picture, and putting it into any empty bucket. (One on which q.bucket.body is not defined yet). The other atom provides for the clockwise and counterclockwise assembly of the already-started pictures.

The first atom can be more elegantly written with the "code:" field, Experiment plan: vm/jigsaw-05

(code:(!q.body)*(!q.bucket))   (seq:(q.body.seq+[1,3])%4, bucket:p.body)

The above clearly shows that there are two conditions for the first atom to apply: q.body should not be defined (i.e. the current piece belongs to a picture from which no piece has been moved yet), and q.bucket is not defined (i.e. no piece has been put into the current bucket -- thus, it's empty).

Example 5

This game pieces represent playing cards, with the properties "suite" (= "spades", "clubs", etc) and "value" (= 2,3,...,14). The player must put different suites into different buckets (of his own choice), in the order of increasing values. (I.e. a queen of spades can be put on top of a jack or 10 of 9 of spades, but not on top of a king or ace of spades, and not on top of a card of a dfferent suite).

Note that this game allows stalemates. If, for example, the player put the queen of spades into a bucket before the jack of spades has been removed, then this jack won't be able to ever leave the board. Experiment plan: vm/gs5-example05

To implement this, I made a set of composite image objects, with the attribute "color:" in lieue of "suite:". Visually, the "value" of each object is represented by the number of filled elements in the grid.

(code:!q.color*!q.bucket) (color:q.bucket.color, code:this.value>=q.bucket.value)

Example 6

This comes from Paul's 2021-04-30 message.

Rule_PK2n: alternately clear all of the highest row and then all of the lowest row of the displayed objects (and iterate). The first object can go anywhere, but after that the objects must be placed in clockwise order.
Experiment plan: vm/gs5-example06
1 (pos:T, bucket: [!p*[0.1,2,3],  p+1)
(code: (pos-1)/6 = (last.pos-1)/6),    bucket:p+1)
1 (pos:B, bucket:p+1)
(code: (pos-1)/6 = (last.pos-1)/6),    bucket:p+1)