Last update: 30/10/2000
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 ZRF 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 ZRF is not
trivial.
This document is meant as an addition to those guidelines
given in the included documentation.
(game
(board (grid (dimensions ... )
);end grid
(win-condition ...)
(piece
);end game |
]------------------
<-- action block
]------------------
|
]
] ]-move block ] ] ]
|
Note that everything in a line which follows a ; is ignored.
This is an example for the basic structure of a ZRF file.
It is composed of several blocks that are surrounded by a pair of round
brackets. Each block begins with a keyword, after which the definitions
follow.
The outermost block is the game
block;
it begins in the first line of the file and ends in the last line (that
is, unless you specify variants
of the game or use macros, which would be on the same level as game).
Several sub-blocks need to exist within the game
block; for example, a list of players must
be specified.
There are other blocks like pass-turn
which
are optional. If you don't specify them, default values are used.
Very important in every game are the movement capabilities
of the pieces. You specify them in the moves
block, which is a sub-block of piece. The
move
block itself has an arbitrary number of sub-blocks which describe different
moves of the piece. Unfortunately, the terms used for the critical distinction
between the move block and its sub-blocks are sometimes confused. This
document will conform to a suggestion made by Adrian King: He introduced
the term "action block" to describe a sub-block of move.
But note that you may also find the terms "move line"
or "move block" to describe an action block.
Be aware that the action blocks of one piece are executed
in the order you give them. However, you have no guarantee about the order
the pieces will generate their moves in.
(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"
block 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 block.
Macro calls along with their arguments are always 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 in the macro
is enclosed in 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 an action 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.
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 block.
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 action 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
A lot of boards can be conveniently described using a grid statement. However, when the board is irregular, or you wish to establish a special notation system not supported by grid, you need to use lots of position/links statements. It is alot of work to specify all squares manually; you should instead consider writing a program in Pascal or C that creates all the necessary data as an output.
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 (though not in one that comes with Zillions).
"To" points to the destination of a piece's move, and is reached by (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 before adding. Of course mark and back can 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>)). Accidently leaving the board like that
and thus halting the move is a common source of error.
Conclusion: In order to make sure the move generation
does not halt, use (if (on-board? <direction>)
...) if you are unsure whether a step is possible
from your current position.
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, many of them for making pieces slide like Rooks.
A common pitfall is a wrong assumption about the 'terminal
case'. Look at this:
( (while (on-board? next) (if empty?
add) next ) )
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. It
is usually called 'end', and is linked to be behind the last 'real' square,
so tests like the above one will stop on and ignore 'end'.
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 (via verify) a
piece is present on the square you use subsequent froms
on. Else you will get an error, or even a crash!
Example:
(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 to the beginner: 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)
The piece who makes a move like that will cause all pieces
in its line to move one step north, except the last one, which will not
move, and thus is 'overwritten' by its predecessor.
Cascade in a while loop is a kind of 'snowball effect': Every piece causes the next one in the chain to move. Anything else would be difficult to implement because you lose the original from-square (i.e. the piece that moved first) with every cascade.
Another important thing to note is that add
does not reset the cascade-list, and there
is no other way to do it manually, either. The list resets automatically
at the beginning of every action block.
This means that every cascade
in one move block makes one additional piece move. Even when you
use add in between, the list of pieces that
move gets longer and longer. Remember this when using cascade
in a loop.
During a drop, cascade works
differently than you might expect. As no from
exists during a drop, each cascade creates a new drop with the off-board
pool as the point of origin. Drops and moves may not be combined.
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 action block in the move block of a piece. Because of this, you
might want to have move blocks that consist of only one action 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 combine several action-blocks to retain the position-flags,
you must not use verify. It
would normally halt one action block, but here it would halt the entire
move block and the piece could not move at all. Instead you must use cascaded
if checks, opening a new level for each verify
that you replace with an if.
This can get confusing. Don't forget to (go
from) after each move generated this way.
See GROUPS for another example
for the use of positional-flags.
Attributes are associated
with pieces. Their initial value can be specified with the (attribute
... ) command when defining a piece. Attributes
are initialized at the beginning of a game. Pieces that enter the board
also start with their initial attributes. Note that drops are the only
way a new piece can enter a game (where add-copy works like a drop)! That
means if a piece promotes, it retains its old attributes and is
not
initialized
as a new piece.
Note that when checking attributes,
you don't need a special keyword, instead you just give the attribute name.
This checks the piece on the current square for the attribute. If a piece
does not possess this attribute, it is assumed to be false.
Examples for testing:
Flag: (verify
(flag? test) )
Position-Flag: (verify
(position-flag? test) )
Attribute: (verify
test)
To put these three kind of indicators to a good use, you
must know about their respective advantages and disadvantages.
Position-flags are very short lived. Their scope is limited
to the current action block. This means you cannot use them to control
game flow at a larger scape. Instead, mark special board positions of the
current move with them. For example, they can be used to identify the from-square,
as there is no command like (verify from?)
in ZRF. You need to know the from-square when
scanning through the board presuming the current move has already been
made, so you treat from as empty
and to as friend.
Flags on the other hand, live very long. In fact, they
never reset and change their state as soon as a set-flag
command is encountered. They can be very useful to remember whether a certain
event during move generation has occured. However, you must be careful
to reset them at the right time, usually at the beginning of one action
block. It is possible to use flags even beyond one single action block,
though it can be problematic. Always keep in mind that they don't require
the move they are in to be played in order to change state; they will change
even when the move is only generated.
Because of that, you need attributes. They are better
suited for storing information that must be available beyond the current
turn, because they only change state when a move is actually played. That
however, is also their disadvantage: They are too slow to be written and
read out during the same turn. If you want that behaviour, use flags.
ABSOLUTE-CONFIG / RELATIVE-CONFIG
A piece-type must be specified with
(absolute-config) and
(relative-config) winning conditions. 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 all of the given squares.
CONCLUSION: Instead of <any-piece>, just list all piece types here.
You can also specify a zone
instead a list of positions, but then just one of the squares it contains
must be filled, not all.
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
equals 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.)
In a cascade,
the
first pieced in the chain usually receives the next partial move. In unusual
cases, though, where a piece caused a move without moving itself, the next
piece moves again.
4. Advanced Concepts
RECYCLE
The recycle commands 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 games like Shogi because pieces return to the pool of their own colour, i.e. 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) or (change-type
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) or
(change-type 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 (the latter 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 limitation by generating null-moves, i.e. a move to the starting square of a piece. However, you cannot simply give
(moves (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
(moves ((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 only visible on the board when picking up
a piece.
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 move counts twice in Zillions' eyes, thus leading to Zillions
assigning an artificially high value to this piece. If this happens by
accident, the AI will probably play weaker with this wrong value. However,
you can deliberately give a piece double moves if you notice that Zillions
assigns too little points to it. Double moves then become a way of slightly
tweaking the evaluation of the AI.
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/take
away 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, or the appropriate (pieces-remaining
...) condition created by this action.
This works, and the AI recognizes it, too; however, it is possible that you notice a weaker performance of the AI if your conditions become too complex.
A possible implementation of a complex winning condition is shown in the GROUPS-example below.
Another example can be seen in Ninuki-Renju, which is
a variant of Go-Moku and is on the CD.
In this game, all players start with a dummy piece called
Capture-10 on a dummy square. Once five pairs of stones are captured, this
dummy piece is removed, which causes the side it belonged to to win because
of the (pieces-remaining 0 Capture-10) goal.
This method is probably even better
than the one demonstrated in the GROUPS-example because it does not involve
a neutral player, but note that it requires as many dummy squares/pieces
as players present in the game.
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.) -->
Example
The basic way how groups are formed is this: One or more
position-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,
or checking which enemies are captured by surrounding as in MiniGo.
In MiniGo, this is accomplished 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': 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 in
this ancient egyptian game. 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!
You can also have a 'Referee' player perform the same
move regularly (e.g. to reset a counter after each move). Using a random
player for this task has the advantage that no confusing entry appears
in the 'Choose Players' dialog.
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.
Another way to differentiate between colours is used in
Ninuki Renju (variant of Go-Moku). When checking the complex win-condition,
the correct dummy square for the colour currently moving must be found.
Note how this is accomplished:
- The direction right leads
from one dummy square to the next.
- We go to the first dummy square
and say (while enemy? right) , which
moves us to 'our' square. (of course this only works with appropriate pieces
on the squares)
COUNTING
Zillions does not support any integer
counting.
But still, some goals involving
that something is counted can be simulated. Have a look at Ninuki Renju
(variant of Go Moku) to see how it counts to five with booleans only. It
uses five flags
and five piece-attributes.
The flags store the number of captures the current move caused and
are reset to false at the beginning of every move. The attributes store
the values for the whole game and cause a win once five is reached
by capturing the counter (see COMPLEX WINNING CONDITIONS for more).
Macro bump-attributes increases
flag-count by one and is called after every capture of a pair of stones.
Macro count-caps increases the
piece-attribute-counter by the number currently stored in the flags.
CAPTURE WITHOUT MOVING
The usual way of capturing an enemy without moving one of your pieces is this:
(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.
Together with partial moves, this can also be used creatively
to remember where captures took place. Sometimes, it can be a problem that
you have no way of telling where pieces were taken away via capture
last turn. However, when you capture an enemy by first giving your piece
a partial move in which it copies itself onto the enemy, and simultaneously
captures its copy, the square on which this capture occured will be last-to!
SPLITTING PIECES
With a little work, it is possible
to have "combined" pieces in Zillions, i.e. pieces which are actually "composed"
of more than one piece. Though this feature is not directly supported,
it can be simulated with a trick: You just define a new piece-type for
the combined piece.
Combining pieces should be easy:
Just have one of the partial pieces capture another, and change its type
on the destination square. The simplest way to do this is the promotion
syntax (add <combined-piece-type>).
Splitting the pieces is a bit more
difficult. You have to change the piece type on both the from
-square and drop a new piece on the to-square
using add-copy. Have
a look on this example macro, which changes the move of a Chess Queen,
so that it may split into its components, Rook and Bishop. In this example,
it spawns a Bishop and leaves a Rook behind.
(define qslide
(
$1
(while empty?
to
(go from)
(change-type Rook)
(go to)
(add-copy Bishop)
$1)
(verify not-friend?)
to
(go from)
(change-type Rook)
(go to)
(add-copy Bishop)
))
(Note that this macro will only work correctly with ZoG versions v1.1.1+)
For a practical example for combined pieces, have a look
at my beta implementation of Vyremorn Chess, where the mounting and dismounting
of Disks is handled this way.
MULTIPLAYER GAMES
Games with more than two players introduce several new
problems to ZoG.
The first is a problem of the AI. The AI has no other
choice as to treat all other players as enemies, assuming the worst case
scenario that they unite against the current player. Possible and perhaps
temporal alliances between players (aka the "petty diplomacy problem")
cannot be taken into account for ZoGs calculations.
This has the side effect that Zillions' board evalution
is always negative, and it will wholeheartedly welcome all draws; you should
concerning turning draws into losses for multiplayer games.
A second unfortunate effect is that there is no longer
a way to identify which player a piece belongs to. In two player games,
you can identify the current player with a zone
test; all enemies must then belong to the other player. In multiplayer
games however, an enemy might belong to any player; you cannot tell the
difference.
One way to work around it is to use different piece names
for every player: Player Red gets RedPawn, player Green gets GreenPawn.
The pieces just differ in their names, otherwise they are identical. However,
this method is quite unelegant and can get tedious if your game has many
piece types.
You can also try to structure your ZRF in such a way
that every player can perform all necessary tests for himself, avoiding
the need to identify the exact player of an enemy piece.
Another problem of multiplayer games has to do with loss-conditions.
Zillions will terminate a game once a loss-condition
is met, displaying player X has lost. However, you sometimes want the game
to continue; player X is out, but the others might still compete for the
win. To achieve this, don't use loss-conditions.
Instead, have a dummy piece which indicates whether the player is still
in the game. The piece can capture itself once the player is out. I tried
such a concept in King's Palace v1.1.
Another issue to take care of is to pass the turn for
the player who is out. An automatic pass can be achieved via (pass-turn
forced) ; of course you need to disable all moves when a player's
indicator is no longer present.
PARAMETERS
Remember that in macros placeholders like $1
are simply replaced with the parameters the macro is called with. This
can be used in several creative ways.
For example, you can distinguish between two different
cases in the same macro by handing true or
false
over as a parameter. This is used in Hasami Shogi. In the macro jump-or-slide,
parameter $4 determines whether a capture
around the corner is allowed.
Another interesting method is to use parameters as part
of flag or attribute
names. For example, the command (set-flag searchfor$1
true) can refer to different flags depending on what piece type
you specify when calling the macro it is used in. Call it with (macro-name
Rook), and the command will be (set-flag
searchforRook true).
MAXIMIZING SPEED
It's a good thing when only correct moves are generated by your ZRF, but it's even better when they are generated fast. The less board positions the AI can generate during it's turn, the worse it will play. Very slow ZRFs might even produce a noticeable delay before every single turn. Get some hints in this sections how to avoid that.
ONCE PER PIECE TESTS
When testing for something expensive
applicable before the move of a piece, be sure to do it only once
per move block, not once per action block. Do all your once-per-piece-things
in the topmost action block in your move section. This topmost action block
generates no moves, it just sets persistent flags for the rest of the move
block to check.
Although it is not really guaranteed
by the program that all action blocks are executed in the order you give
them, this trick relies on it nonetheless. It usually works. If it doesn't,
don't say you were not warned, and have a watchful eye for bugs that might
be caused by the blocks being executed in the wrong order.
ONCE PER TURN TESTS
Sometimes it is even too expensive
to scan the board once per piece, and you want some things checked only
at the very beginning of your turn; you want a certain action block to
be executed before all others.
As the order pieces generate their
moves in is undetermined, you cannot just put a dummy piece with this move
above the other pieces in your ZRF, you need another way to distinguish
your block from the others. Move-priorities
can serve you here; action blocks that belong to move-type with priority
will be generated first, so you assign a special 'dummy' top-priority to
your action block.
You need to make sure your 'pre
move generation' block doesn't add moves
itself; their faked priority would prevent your normal moves. However,
you are free to perform all sorts of checks and (especially important)
you can set and reset persistent flags for all the action blocks to follow
as a kind of initialization.
It doesn't really matter which
piece gets this special action block, as long as every player is guaranteed
to have exactly one of this kind. Royal pieces lend themselves to this
purpose, but dummies work as well.
A WORD OF WARNING
Always remember: Neither the priority
technique, nor flags persistent between action blocks, nor some other tricks
described here are officially supported by Zillions. They have been observed
to work at least once, but nobody guarantees they will work for your
game. Just experiment. The worst thing that can happen is a program crash.
[Not really. The worst that can happen is that everything appears
to work fine, although a bug is lurking deep deep down in your ZRF!]
COMMON ERRORS
Comments? Questions? Additions? Write to LordX@gmx.net
This text is provided without warranty. You use it at
your own risk.
© Jens Markmann