The ZRF Language
 Last update: 17/04/99

This page is no longer updated!
The new URL of this page is
http://cruise.de/jens.markmann/zrf.html

The program Zillions of Games is not only a collection of almost 300 entertaining games from all over the world. Its most outstanding feature is the built-in scripting language which describes the rules for each game played. This language is very flexible and can describe almost any perfect-information abstract board game.
However, due its versatility, the grammar of the language (called ZRF for the rest of this document) is not trivial.

This document is meant as an addition to those guidelines given in the help file at ftp.zillions-of-games.com.
 
 
 (game 
    (board 
        (grid 
            (dimensions ... ) 
  
        );end grid 
    );end board 

 (win-condition ...) 
 (players ...) 
 (turn-order...) 

 (piece 
  (name ...) 
  (moves 
     (n add)  
     (s add) 
     (e add) 
     (w add) 
  );end moves 
 );end piece 

);end game

]------------------ 

<-- move block/line 
<-- move block/line 

]------------------ 
 
 
 

] 
] 
]-move section 
] 
] 
] 
 
 
 
 
 
This is an example for the basic structure of ZRF. Note that everything in a line which follows a ; is ignored. However, every ZRF-file must end with a bracket. You may not add comments after that. Thus, the above ;end game would cause an error.

 

1. Macros

You can write in ZRF without using any macros, but it is very advisable to use them as they save you a lot of typing and keep the structure of your script more logical. Have a look at this:

(define step
 ( $1
   (verify empty?)
   add
 )
)

This is the definition of the macro "step". Note the $1 in the command list. It refers to the first argument which is given to the macro.
You use it like this in the "move" section of a piece:

(piece
 ...
  (moves
    (step n)
  )
)

Zillions now takes the  definition of "step", replaces $1 with n and puts the entire command list from the definition into the move section.
Macro calls along with their arguments are enclosed in brackets.

You can use more than one argument in a macro call ($2, $3 ...), and they need not necessarily be directions, you can also pass over piece types or flag names.

ATTENTION:
As always in ZRF, the correct use of brackets is very important in macro definitions! Remember that the macro call is simply replaced with the contents of the definition.
So, in the above example, the command list has its own pair of brackets, and the macro can be called with (step n). It can, however not be called with (n (step n)) to make 2 steps northward, as the inner brackets of the definition would appear in the middle of a move block where they do not belong.
If you want to use a macro both stand-alone and in the middle of a block, define it without inner brackets:

(define step
  $1
   (verify empty?)
   add
)

You can now use (n (step n)) as well as the stand-alone call ((step n)).

By the way, note that carriage returns are not important to the compiler.
(define step $1 (verify empty?) add)
This definition is equivalent.
The compiler is case-sensitive, however. Try to avoid all upper-case letters except in piece names.

 


2. Move Generation (introduction)

This is the most important task of your ZRF file, so plan it carefully!
You tell Zillions how pieces move in the (moves ...) and (drops...) blocks of the piece section.

The move blocks of course apply to all pieces on the board. At the start of each block, the current square is set to the from-square.

The drop blocks only apply to pieces in the off-board pool. This a special pool where all your (off ...) pieces from (board-setup...) start and pieces recycled with (recycle-capture) go to. These pieces are never visible.

(piece
...
  (moves
    (n add)
  )
)

This is simple example of a move block. Zillions interprets it like this:
--Start on the square where the piece stands.
--Go 1 square n (north).
--The piece may move to this square.

Add (or add-partial, add-copy, ...) generates a move possibility to the current square. A piece can choose between as many moves as add commands are present in the move section.

Well, quite good so far, but usually pieces are not allowed to capture friendly pieces on their destination square.
Let's add this:

(piece
...
  (moves
    (n (verify not-friend?) add)
  )
)

Zillion thinks:
--Start on the square where the piece stands.
--Go 1 square n (north).
--If there is a not-friendly piece here, go on. Else stop at once, the piece may not make this move.
--The piece may move to this square.

Note that if a (verify ...) test fails, the generation of this move is halted immediately! The rest of this move block is skipped, and the next block in the section is processed.
This does not happen if you use (if...) instead.

(moves
  (n (if enemy? capture) n (verify empty?) add)
)

Can you see what that does?
--Go n.
--If there is an enemy piece here, mark it for capture. If not, do nothing, but CONTINUE either case!
--Go n.
--If this square is empty, continue. If not, halt.
--The piece may move to this square.

This also explains the use of capture: It marks the piece on the current square (whether friend or enemy) to be removed from the board when the move is made. It is used to capture pieces which are not on the destination square of the move. (Example: 9 Men's Morris)

A word on brackets: As a rule of thumb, everything that belongs together goes into one set of brackets. This is especially true for arguments that belong to condition-tests.
Examples:
(verify empty?)
(verify (empty? a1) )

and, not and or have a bracket before them, the conditions which follow do not require extra brackets by themselves, only if they have arguments.
Examples:
(verify (and not-friend? attacked?) )
(verify (and (not-friend? a1) attacked?) )
(verify (and (not-friend? a1) (attacked? b2) ) )


3. Basic concepts
 
BOARD DEFINITION
Almost any board can be described using a grid statement. Creating a board completely by writing position / links should generally be avoided and only used for boards that do not possess any symmetry at all because it is a lot of work.
For a smart use of grid , have a look at Chinese Checkers. It utilizes a large "diagonal" grid (diamond-shaped) in which the ranks have an x-offset (similar to the "3D" effect in 3D-TicTacToe). To give the correct star-shape to the board, the unwanted positions are deleted with kill-positions.
With this method it is easy to define a hexagonal board on which each cell has 6 neighbours. The Chinese Checkers board is basically hexagonal, too.

a1  b1  c1  d1  e1  f1  g1
  a2  b2  c2  d2  e2  f2  g2
    a3  b3  c3  d3  e3  f3  g3
      a4  b4  c4  d4  e4  f4  g4
        a5  b5  c5  d5  e5  f5  g5
          a6  b6  c6  d6  e6  f6  g6
            a7  b7  c7  d7  e7  f7  g7
 
 
 

  (grid
     (start-rectangle -48 8 -20 36)  ; note that the starting square is offboard. Having it onboard creates an unnecessary and ugly empty border
     (dimensions
      ("a/b/c/d/e/f/g/h/i" (28 0) )
      ("9/8/7/6/5/4/3/2/1" (14 28) )  ; the "14" is the x-offset
     )
     (directions                                             ; each square is linked with six neighbours
      (nw 0 -1) (ne 1 -1)
      (sw -1 1) (se 0 1)
      (e 1 0) (w -1 0)
  )

When designing new boards for games you wish to share with other people, you should always consider the actual size the board has in your Zillions window.  Other people might use a different Windows resolution, and your board might not fit on their screen. In this case, try to make the board as small as possible retaining a playable size. Now users with small screens can keep it like that, all others can simply 'enlarge' the image (Ctrl-E).
 

 
 
 
 

 

FROM-TO
"From" points to the square where the move of the piece starts. Go to it with (go from). At the beginning of the move, from is the square the piece currently stands on. If you change the from square without saving the old one via cascade, your piece actually enables another piece to move, but does not move itself. This behaviour can be found in some chess variants (not on CD).

"To" points to the destination of a piece's move (go to). When used without cascade , its only purpose is to save time and keep the code compact. You can set the to pointer to a square, then continue to verify other conditions and still halt the move. Of course mark and back accomplish the same.

The commands to and from set the pointers to the current square, and as usual are activated once add is used, which generates the move.
Note that these two lines do exactly the same:
(n e to w e s s add)
(n e add)

NOTE: To be honest, these two commands only do the same in the middle of the board, for if you use a <direction> which does not exist from your current position, the generation of the whole move is halted. It is the same effect you would get with (verify (on-board? <direction>)).
Conclusion: In order to make sure the move generation does not halt, use (if (on-board? <direction>) ...)
 

 

CASCADE
Cascade is used to move more than one piece in one turn. Set the first piece's destination with to, go to the square where the second piece stands, and use cascade. This saves the "to" and "from" pointers, and you can use them again. All moves take place at once when you do an "add".
Make sure (verify) a piece is present on the square you use subsequent froms on. Else you will get an error, or even a crash!

(n (verify enemy?) to cascade from s to add)

This exchanges a piece with an enemy north of it. The to before the cascade is optional. If not set explicitly, to is set to the current square when cascade is used.

Note: Remember that (to cascade from s to add) is simply a sequence of commands. Do not read something like "from south" or "add to". Read "to, cascade, from, south, to, add".

Some chess variants have pieces that may push other pieces, capturing them by pushing them of the board.
This is the way to script it:
(n to (while (and not-empty? (on-board? n)) cascade from n to) add)
 
 

 

WHILE
While is the only loop command in ZRF. As long as the condition given is true, the command list is executed. Be sure that the loop stops at some point, or you will get the eternal loop error. Note that it is possible that the command is not executed at all, not even once! This can happen if the condition is false from the beginning.
There are various examples for while in Zillion-ZRFs, most of them for making pieces slide like Rooks.
Look at this:
( (while (on-board? next) (if empty? add) ) )

This is supposed to add moves to all empty squares on the board, assuming they are all interconnected by the "next" direction. But what happens once the current square is the last square on the board? (on-board? next) becomes false, and the last square in the chain is never checked nor added! To avoid this, create a "dummy" last square off screen.
 
 

 

FLAGS / ATTRIBUTES
Flags (set-flag ...) are global. Be sure to use unique flag names in your script so you won't mix them up. Note that unlike piece attributes, flags change instantly once the (set-flag ... ) is encountered. They do not wait for an add , neither do they wait for the move they are in to be actually played. Flags need no further initialization.

Positional flags (set-position-flag ...) are associated with positions and reset to false at the beginning of each line in the move section of a piece. Because of this, you might want to have move sections that consist of only one block in certain games, so your position-flags do not reset. Have a look at Ultima as an example for this. It uses position-flags to create 'lines' originating from the Coordinators and locate their intersections.
If you have one-line move sections, you must not use verify. It would normally halt the generation of one move line, but here it would halt the entire section and the piece could not move at all. Instead you must use multiple-level if checks, opening a new level for each verify that you replace with an if. (This is definitely an advanced concept.) Don't forget to (go from) each move generated this way.
See GROUPS for another example for the use of positional-flags.

Attributes are associated with pieces. They must be initialized with the (attribute ... ) command when defining a piece. Attributes are never reset, but they disappear when the piece they belong to is captured or changes type.
Note that when checking attributes, you don't need a special keyword, instead you just give the attribute name.

Examples for testing:
Flag: (verify (flag? test) )
Position-Flag: (verify (position-flag? test) )
Attribute: (verify test)
 
 

 

ABSOLUTE-CONFIG / RELATIVE-CONFIG
A piece-type must be specified with (absolute-config) and (relative-config). The simple declaration <any-piece> is missing from ZRF.
However, it is possible to specify more than one piece type.

(win-condition (White Black) (absolute-config Knight King (b2 b3 b4)) )

It means that a Knight or a King must be present on the given squares.

CONCLUSION: Instead of <any-piece>, just list all piece types here.
 
 
 

PARTIAL MOVES AND MOVETYPES
Partial moves are used in many games, usually in the form (add-partial <move-type>). This means the player's turn is not yet over after this move, he may (or must, depending on the state of (pass-partial) ) make another move with the same piece.
add-partial (no brackets!) allows the player to make a move of any type. If you give a <move-type>-argument, the player is limited to this kind of move.
Example: Checkers. There a two move-types for pieces, jumptype and nonjumptype. When his turn starts, the player normally has the choice which move to make, but after he has captured, he is limited to more capturing moves with (add-partial jumptype). If this limitation not existed, he could first capture and then step with his pieces.
Checkers is also a good example of move-priorities . As you know, you must capture a piece in Checkers if you can. This is simply explained to Zillions in the line (move-priorities jumptype nonjumptype), meaning if there are moves of the first type available, they must be made. If not, moves of the second type must be made, and so on.

Sometimes, you might want to check whether you are currently within a partial move or just starting a regular one. To do so, remember that within a partial move, from = last-to.
(Where last-to is always the to of the last move, whether this was yours or your enemy's, whether it was partial or not.)
Currently, partial moves seem to be a bit difficult to handle for the Zillions-AI. It sometimes has troubles foreseeing the consequences of such moves. Thus use them sparsely.
 
 


3. Advanced Concepts
 
 RECYCLE
The recycle commands are not well explained in the manual. They mean the following:

(recycle-captures false): All pieces captured disappear from the game.
(recycle-captures true ): All pieces captured go to the off-board pool. This is the same pool where (off...) pieces start in the (board-setup...) section.

These commands are not useful for the game Shogi because pieces return to the pool of their own colour, ie White pieces captured by Black go to the White pool. In Shogi, pieces captured must be converted to the colour which captured them. That is why the Shogi-ZRF needs a quite large section to manage the prison manually!

(recycle-promotions false): Pieces added to the board by promotion, for example with (add Queen), are generated and appear from nowhere, as they do in Chess.
(recycle-promotions true): Pieces added to the board by promotion, for example with (add Queen), come from the player's off-board pool, where they must be present or the promotion cannot take place.

When both recycle flags are true, and the pool is empty at the beginning, pieces can of course only be promoted to pieces already captured. This is the case in Burmese Chess or in C. Freeling's Grand Chess (not included with Zillions).
 
 

 

SIMULATED PASSING
With (pass-turn ...) or (pass-partial ...), it is not possible to allow passing only under certain conditions found on the board. You can circumvent this situation by generating null-moves, ie a move to the starting square of a piece. However, you cannot simply give

((add))

as a move block, because Zillions does not generate moves which do not change the board configuration. Instead, give the piece which can make a null-move a dummy attribute and the move block

( (set-attribute whatever true) add)

You can now effectively pass by picking this piece up and putting it back on its starting square. Don't forget to turn off the 'real' passing and to inform the player of this special passing move, as it is not visible on the board.
 

DOUBLE MOVES
It is generally possible in Zillions to generate moves which lead to the same square.
Example:
(n ne (verify empty?) add)
(ne n (verify empty?) add)
Both move blocks generate a Knight's move to the same square. Zillions v1.0.2+ will notice this, and will not bother you with a menu asking to select one move.
However, there are two things to note about double moves:

First, it is still possible that a menu with identical choices appears.
(n ne (verify empty?) (set-attribute nevermoved? true) add)
(n ne (verify empty?) add)

Both moves end on the same square, but one changes a piece attribute, the other does not. The menu will appear, the entries are unfortunately identical, and the only way to tell the difference between them might be to guess by their order. You will generally want to avoid this situation. Note that if in the above example (set-attribute nevermoved? true) would be replaced by (set-flag nevermoved? true) , the move menu would not appear. The flag is set anyway, so these two moves are in fact identical.

The second thing to remember about double moves is a bit more subtle and concerns the Zillions AI. Zillions assigns point values to the pieces which you can see under the description when right-clicking on them. This value is based on the number of possible moves for this piece, not on the number of accessible squares. This means a double moves counts twice in Zillions' eyes, thus leading to a misjudgement and weaker play of the AI.

CONCLUSION: Avoid generating double moves.
 
 

COMPLEX WINNING CONDITIONS
With a trick, it is possible to define very uncommon and complex winning conditions, involving many (verify...) or (if...) checks that are not possible in a simple (win-condition...)-goal.
Try the following: Make the check after every move, for example by defining a macro that is called before every add. Should you find out that the game is won during your macro, place a certain piece on a certain dummy square off-screen, using this piece as a "flag". The actual (win-condition...) would then be the so created (absolute-config...) on this square.
This works, and the AI recognizes it, too, but I do not know whether the AI will really work towards it several moves in advance.
A possible implementation of a complex winning conditions is show in the GROUPS-example below.

 

GROUPS
Several board games make use of the concept of groups. A group is a number of interconnected pieces, i.e. not separated by empty squares.
An example for a game which uses groups is Go. Fortunately, MiniGo comes with Zillions, meaning it is indeed possible to check for the presence of groups in ZRF.
However, it might be a bit difficult to understand this concept and vary it in order to use it in own creations, so I will present an example macro which 'counts' the number of groups present on the board. It makes the player win if he has joined his pieces together in one group.
(Actually, the macro is simplified, meaning it does not fulfill its purpose in really all cases, but it should be sufficient to explain how groups can be checked.)

The basic way how groups are formed is this: One or more positional-flags are set on specific locations, then they are 'spread' to include the whole group. This is done by going through the board several times, until no further 'spreads' are possible.
On a second approach, the so created groups can then be checked for a lot of purposes, for example 'counting' them as shown here, or checking which enemies are captured by surrounding as in MiniGo (see below).

The following example macro is called after the move of a piece has been set with to , but before the add.
 

(define check-for-group

(go to) mark

; This macro is used before every add to check for the victory condition.
; Unfortunately, this means that for Zillions this very move has not yet taken place.
; The piece is still presumed to be on its from-square, and its to-square is believed to
; be empty. Therefore, we will mark the to-square and treat it as friend.
; Theoretically, we would have to do the same with the from-square, too, in order
; to treat it as empty. However, there is only one 'mark' in Zillions, which means
; we would need additional positional-flags if we want to manually manage both squares.
; It would make this example unnecassarily complicated, which is only there to illustrate
; the concept.
; MiniGo addresses this problem by manually setting the to-square's empty (safe) flag to false
; after the initialization. If Go pieces didn't drop and thus had a from square, its empty
; flag would have to be set to true.

a1
(while (and not-marked? not-friend?) next) ; find the first friend.
                                           ; Remember we need to treat 'marked square' as friend.
(set-position-flag firstgroup true)        ; mark the first friend

; Note: Mark is this context of course not refers to the ZRF-command 'mark'. We use
; positional-flags to mark groups.

a1
(set-flag changed true)   ; in order make outer while start
(while (flag? changed)    ; as long as something on the board changes...
 (set-flag changed false)  ; nothing has been changed yet
 a1
 (while (not-position? end) ; go through the whole boards
    (if (and (or marked? friend?)
             (not-position-flag? firstgroup) ; if we find an unmarked friend...
             (or                         ; ... who is adjacent to a piece in the first group...
                (position-flag? firstgroup n)
                (position-flag? firstgroup s)
                (position-flag? firstgroup e)
                (position-flag? firstgroup w)
             )
         )

     (set-position-flag firstgroup true) ; ... this piece also belongs to the first group
     (set-flag changed true) ; something has changed on the board
    ) ; end if
    next
 ) ; next position
) ; next run

; Our first group on the board has been marked with the position flag firstgroup.
; Obviously, if we find one of our pieces without this flag, it must be part of the second group.

a1
(set-flag wehavewon true)  ; presumes we have won until the opposite is proven
(while (not-position? end)  ; go through the whole board
 (if (and (or marked? friend?)
     (not-position-flag? firstgroup)) ; if there is an unmarked friend ...
   (set-flag wehavewon false) ; ...we haven't won
 ) ; end if
 next
); end while

; Now the flag wehavewon indicates whether we have won.
; To make Zillions recognize this as an actual win-condition,
; we can convert the dummy piece Goal on indicator to our side.

(if (flag? wehavewon) (change-owner indicator) )

)

(win-condition (White Black) (absolute-config Goal (indicator) ))

... assuming the game starts with a neutral dummy piece called 'Goal' on the dummy square 'indicator' (a complex winning condition). This macro requires a normal, chess-like board, whose squares are all connected with each other via the next direction, beginning with a1 and ending with end (dummy off-board).
 
 

Capturing groups by surrounding in MiniGo is done in these basic steps:
1. Initialize p-flag 'safe': Go through whole board -> square is safe if empty
1a. (to is not safe)
2. Initialize p-flag 'danger': Go through whole board -> enemy is in danger if adjacent to to
3. Spread safe / danger to orthogonal adjacent enemies as long as possible on whole board
4. Go through whole board -> capture all enemies which are in danger but not safe
 
 

 

RANDOM/REGULAR EVENTS
It is possible to use random elements in games. This can be seen in Senat.
It uses a player called ?Dice-Roll, and the important thing here is the question mark. All players whose names start with a question mark do not appear in the "Choose Side" dialog and automatically make a random move when it is their turn. As in Senat, this is probably best used to throw a dice. The drawback is that ZRF has no counting whatsoever, so every possible dice value must be checked manually.
ATTENTION: The frequency of the "dice-rolls" in Senat is not that of a normal dice! You can see that some numbers are "added" more often than others. This is done to simulate the throwing of rods. When you add the appearence of the value "4", for example, twice, it will appear twice as often as a value added once.
Conclusion: For a normal dice, add each value only once!

This can also be used to have a 'Referee' player perform the same move regularly (ie to reset a counter after each move). You only need to give this random player exactly one possible move.
 

 

WHICH COLOUR AM I?
Perhaps you might want to know at some point during the move generation whose turn is currently being generated. You can see an example for that in Shogi, although the test is never actually used.
It is simply a zone test which is helpful here, as a zone can have the same name but different positions for each player. So, if (in-zone? promotion-zone a8) is true in Chess, you are White, if not, you are black.
 

 

CAPTURE WITHOUT MOVING
There are two basic ways to capture an enemy without moving.
(Both examples capture the enemy north of you)

1. (n (verify enemy?) capture (go from) add)

  This move is only selectable with Smart Moves On. If another of your pieces can also capture the same     enemy in the same turn, this move is not selectable. You better use method 2 in this case.

2. (n (verify enemy?) capture add-copy)

  This does the following: It creates a copy of your piece on the target square, capturing the enemy there. Then the copy is captured itself. This move has always priority during selection because it is internally considered a drop. I suggest you turn (animate-captures) off, else you'll see your own piece fly away.
WARNING: In versions 1.0.1 and below, the animation code does not support this move. You must turn (animate-drops false), or ZoG will crash. This has been fixed in 1.0.2.

 

COMMON ERRORS
- Close all brackets that you open! By far the most common mistake.
  Advice: First create the framework of brackets, then fill them.
 

- It is sometimes difficult to follow which piece is where during the creation of a move. Note, for example, that the (set-attribute ...) command refers to the piece on the current square after the move is made, and that the to square remains empty before the add.


Back to The Art of Games
Comments? Question? Additions? Write to LordX@gmx.net
Visitors: a few... (no new counter yet)