How to Program Variants with Multi-Move Pieces in Game Courier
This tutorial presumes that you already understand how to use GAME Code to enforce and display legal moves. These subjects are covered in greater depth in these earlier tutorials:
- How to Make Your Game Display Legal Moves in Game Courier (Nov 2015)
- How to Enforce Rules in Game Courier (Jan 2016)
- Game Courier Developer's Guide
Note: This tutorial is not the same as the tutorial for programming multi-move variants in Game Courier. That tutorial is for programming variants with multiple moves per turn. This tutorial is for programming variants with a single move per turn and pieces that can move multiple times per turn (i.e. the Lion in Chu Shogi).
This tutorial is based on code from the rule-enforcing preset for Suzumu Shogi.
Let's being with the Pre-Game code. Since moves are written as lines of GAME Code, the code used to enforce rules can be greatly simplified by restricting user-input to a very narrow range. For Suzumu Shogi, the code for this looks like this:
setsystem maxmove 3; ban commands allmoves; allow moves 1 captures 1 moves 2 captures 2 pass 2 promotions 2 promotions 3;
Although this is a single-move variant, the maximum number of moves is set to three to account for double moves and promotions. It then bans all user-input and selectively allows certain types of moves for the moves allowed to a player. The first move may be either a move or a capture. The second move may be a move, capture, promotion, or turn pass (the second part of a double move is rarely mandatory). The third move must be a promotion. The lesson here is to not blindly copy the code for this game without understanding it. You need to think about what is going on with your game and set the allowed moves accordingly.
Next, let's take a look at some of the piece functions. In this example, we will be looking at the functions for the Queen and the Free Eagle.
def fk fn r #0 #1 or fn b #0 #1; // def fk checkride #0 #1 0 1 or checkride #0 #1 1 1; def fkL merge rays #0 0 1 rays #0 1 1; def fe cond var firstpart (fn fk #0 #1 or checkleap #0 #1 0 2 or checkleap #0 #1 2 2) (checkleap #0 #1 1 1) and #0 and #1; def feL mergeall rays #0 0 1 rays #0 1 1 leaps #0 0 2 leaps #0 2 2;
The first two functions (fk and fkL) are written as normal piece functions for a chess Queen. The second function is used for displaying legal moves. The last two are where things get interesting. The first piece function for the Free Eagle (fe) returns a different result depending the value of the firstpart variable. If this variable is true, the function returns the combined powers of the Queen and the Alibaba. If it's false, the function returns the powers of a Ferz. The and #0 and #1 clause on the end is there because these arguments must appear outside of parentheses at least once. The second function (feL) returns the Free Eagle's powers on the first part of its move since this move also contains the Ferz move from the second part of the double move.
IMPORTANT: When writing the piece function for legal move generation for a custom multi-move piece, make sure it contains all moves a piece could possibly make for all parts of a move. For example, if you wanted to make a piece that moves like a Ferz and can move away from the destination square as a Dabbabah, the functions would look like this:
def fd cond var firstpart (checkleap #0 #1 1 1) (checkleap #0 #1 0 2) and #0 and #1; def fdL merge leaps #0 1 1 leaps #0 0 2;
For pieces whose moves have side effects, you will need a function and a subroutine. To see what I mean, let's look at the piece functions and subroutines for the Soaring Eagle and the Vice General:
// Soaring Eagle Functions def se cond var firstpart (checkride #0 #1 0 1 or checkaride #0 #1 -1 -1 or checkaride #0 #1 1 -1 or checkleap #0 #1 1 1 or checkaleap #0 #1 -2 2 or checkaleap #0 #1 2 2) (sub SE #0 #1) and #0 and #1; def SE cond var firstpart (checkride #0 #1 0 1 or checkaride #0 #1 -1 1 or checkaride #0 #1 1 1 or checkleap #0 #1 1 1 or checkaleap #0 #1 -2 -2 or checkaleap #0 #1 2 -2) (sub SE #0 #1) and #0 and #1; def seL mergeall rays #0 0 1 rays #0 1 1 leaps #0 2 2; def SEL mergeall rays #0 0 1 rays #0 1 1 leaps #0 2 2; // Soaring Eagle Subroutine sub SE from to: if > slope origin dest 0: return checkaleap #from #to 1 1 or checkaleap #from #to -1 -1; else: return checkaleap #from #to 1 -1 or checkaleap #from #to -1 1; endif; endsub; // Vice General Functions def vg cond var firstpart (sub BG #0 #1 and cond empty #0 capture (not empty #1) or checkride #0 #1 1 1 or checkmaxsteps #0 #1 3) (checkleap #0 #1 0 1 or checkleap #0 #1 1 1 and == var ori #1) and #0 and #1; copyfn vg VG; def vgL mergeall rays #0 1 1 leaps #0 0 1 leaps #0 0 2 leaps #0 1 2 leaps #0 2 2 leaps #0 0 3 leaps #0 1 3 leaps #0 2 3 leaps #0 3 3; def VGL mergeall rays #0 1 1 leaps #0 0 1 leaps #0 0 2 leaps #0 1 2 leaps #0 2 2 leaps #0 0 3 leaps #0 1 3 leaps #0 2 3 leaps #0 3 3; // Vice General Subroutine set steps array 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; // This variable serves as a counter for the for loop in the subroutine sub BG from to; local step, cnt, i; if != abs - file #from file #to abs - rank #from rank #to; return false; endif; set step #from; set cnt 0; do; for i #steps: set step where #step cmp file #to file #from cmp rank #to rank #from; if == #step #to: return true; endif; if != @ space #step; set cnt inc #cnt; endif; if >= #cnt 3; return false; endif; next; loop never; endsub;
The first piece function for the Soaring Eagle (se) isn't too different from that of the Free Eagle. But because its Lion moves are linear, the second part of the move needs to call the SE subroutine to ensure that the second part of the moves does not move off the diagonal. The subroutine does this by returning a different moveset depending on the slope from the origin square to the destination square, which is calculated using the formula m = (x1-x2)/(y1-y2). The Vice General can jump over two pieces along a diagonal, but the checkhop operator for pieces like Cannons and Vaos can only jump over one piece, so this needs to be handled by a subroutine. The BG subroutine does this by checking whether the destination square is occupied and stopping at the third occupied square.
Now we get to the stalemated and legalmoves2 subroutines. These subroutines are responsible for displaying legal moves on the board. Before we get to the actual subroutines, there are a few variables I need to cover - the bpieces and wpieces variables, which look like this:
set bpieces p +p gb +gb d +d n +n i +i c +c s +s bt +bt fl +fl g +g de +de rv +rv l +l ky +ky ph +ph sm +sm vm +vm b +b ss +ss vs +vs r +r dh +dh dk +dk cs +cs hf +hf se +se ln +ln q +q bg +bg rg +rg wb +wb fe lh vg gg fd k; set wpieces P +P GB +GB D +D N +N I +I C +C S +S BT +BT FL +FL G +G DE +DE RV +RV L +L KY +KY PH +PH SM +SM VM +VM B +B SS +SS VS +VS R +R DH +DH DK +DK CS +CS HF +HF SE +SE LN +LN Q +Q BG +BG RG +RG WB +WB FE LH VG GG FD K;
These variables contain a list of all the pieces on one side, including promoted pieces (bpieces for Black, and wpieces for White). These variables can, in most cases, be used to avoid having to write a list of all pieces on one side every time you want to reference them. With these out of the way, we now get to the actual stalemated and legalmoves2 subroutines, which look like this:
// Goes through all possible moves, putting all legal moves into the array $legalmoves // Returns size of $legalmoves array sub stalemated kingpos: store; local from piece to; if isupper #kingpos: def friends intersection nolower #wpieces; def friend match #0 #wpieces; else: def friends intersection noupper #bpieces; def friend match #0 #bpieces; endif; set firstpart true; store; // Can any piece legally move? for (from piece) fn friends: for to fn join #piece L #from: if fn #piece #from #to and not fn friend space #to and onboard #to: move #from #to; setlegal #from #to; endif; restore; next; next; // All done. Set $legalmoves and return; return cond count system legalmoves false true; endsub; // Calculates legal moves for second part of turn. sub legalmoves2 kingpos: store; local from piece to; if isupper #kingpos: def friends intersection nolower #wpieces; def friend match #0 #wpieces; else: def friends intersection noupper #bpieces; def friend match #0 #bpieces; endif; // While the royal piece is called the King in these comments, // it may be any piece. set firstpart false; set oldto dest; store; // Can the piece legally move? for (from piece) fn friends: if == #from #oldto: for to fn join #piece L #from: if fn #piece #from #to and not fn friend space #to and onboard #to: move #from #to; setlegal #from #to; endif; restore; next; endif; next; // All done. Set $legalmoves and return; return cond count system legalmoves false true; endsub;
The stalemated and legalmoves2 subroutines are basically the same, with a few small differences. The first thing they do (after creating the required variables) is distinguish friends from foes with the friends and friend functions. Normally the onlyupper and onlylower operators are sufficient here, but because the piece set has pieces that don't strictly match their criteria, I use the clauses intersection nolower #wpieces and intersection noupper #bpieces in the friends function, which return the same results as onlyupper and onlylower. For the friend function, a simple match clause is sufficient.
The next thing the subroutine does is set the firstpart variable to the appropriate value (true for the stalemated subroutine and false for the legalmoves2 subroutine). This variable affects the moves of multi-move pieces. The legalmoves2 subroutine also stores the coordinate of the square that was last moved to in the variable oldto. Once this is done, the subroutines start to calculate legal moves for each piece. The only difference here is that the legalmoves2 subroutine restricts its legal move calculation to the piece that was last moved.
Next, let's take a look at the isdoublemove subroutine. This subroutine is very important to the functionality of any preset with multi-move pieces. It is responsible for determining if a piece can move again after making a move. For the Suzumu Shogi preset, the subroutine looks like this:
sub isdoublemove from to; verify var firstpart; if match moved fd +wb and sub canburn #from #to king: return true; elseif match moved FD +WB and sub canburn #from #to KING: return true; elseif checkleap #from #to 0 1 or checkleap #from #to 1 1 and match moved ln LN lh LH vg VG +ky +KY +ln +LN +bg +BG: if match moved vg VG +bg +BG and capture: return false; else: return true; endif; elseif checkleap #from #to 1 1 and match moved fe FE +q +Q: return true; elseif checkaleap #from #to 0 1 and match moved hf +dh: return true; elseif checkaleap #from #to 0 -1 and match moved HF +DH: return true; elseif checkaleap #from #to -1 1 or checkaleap #from #to 1 1 and match moved se +dk: return true; elseif checkaleap #from #to -1 -1 or checkaleap #from #to 1 -1 and match moved SE +DK: return true; else: return false; endif; endsub;
The isdoublemove subroutine works by testing if a certain piece made a certain move on the first part of a move. If any one criterion is met, the subroutine returns true, and returns false otherwise.
Note: Some conditions, such as the test for whether the Fire Demon can make a double move, are too complex to fit in a single if statement. In these cases, an additional subroutine is required to carry out these tests. For the Fire Demon, this is done via the canburn subroutine. This subroutine returns true if the piece has moved as a King and/or if there is an enemy piece a King's move away from it, and returns false otherwise. The subroutine is shown below.
sub canburn from to kingpos: if fn K #from #to: return true; else: if isupper #kingpos: def enemy match #0 #bpieces; else: def enemy match #0 #wpieces; endif; set north where #to 0 1; // get coords of adjacent spaces set east where #to 1 0; set south where #to 0 -1; set west where #to -1 0; set northeast where #to 1 1; set southeast where #to 1 -1; set southwest where #to -1 -1; set northwest where #to -1 1; if fn enemy space #north: return true; elseif fn enemy space #northeast: return true; elseif fn enemy space #east: return true; elseif fn enemy space #southeast: return true; elseif fn enemy space #south: return true; elseif fn enemy space #southwest: return true; elseif fn enemy space #west: return true; elseif fn enemy space #northwest: return true; else: return false; endif; endif; endsub;
Normally, the Pre-Move code sections are left unused when programming a game. But variants with multi-move pieces like Suzumu Shogi make use of them. Each one should have a single line in them:
What this does is store the board position before the move is made. Then, the Post-Move sections for both games begin with the following two lines:
set mvs explode chr 59 thismove; restore;
The first line here creates an array of all the moves made. It does this with the explode function, using a semicolon as a separator. Since the semicolon is used to end a line of code, it identifies it with the expression chr 59, because the ASCII code for a semicolon is 59. In the second line, it undoes all the moves just made by using the restore command. This restores the position to what it was when the store command was used to store it. What is going on here is that the code needs to handle each move separately. Instead of relying on the usual mechanisms of getting information about a move after it is made, it has to undo it all, then make and evaluate each move individually.
But why go through the trouble of undoing the move? Why not just put code in the pre-move sections to evaluate the moves before they are made? It could be done this way if the code did not need to make the moves as it evaluated them. But that's not going on here. Instead, the code will make the first move by itself, evaluate its legality, and continue to the second move if the first one was legal. If this were all done before the move was made, it would subsequently try to make the same move it had already made, and then complain about trying to move a piece from an empty space. You could put your code in the Pre-Move section if you stored the position before the move at the beginning, stored the final position at the end, restored to the original position before making the move, then restored to the final position in the Post-Move code to account for moves with automatic behavior, such as en passant and Pawn promotion. But this would have you using store and restore twice instead of once. So, it is more efficient to just put store in the Pre-Move sections and the rest of your code for enforcing legal moves in the Post-Move sections.
These lines are followed by this code:
set i 0; set firstpart true; set firstcapture false; eval join "MOVE: " trim elem var i mvs;
The variable i is the counter used to access the right element of the mvs array.
The firstpart variable is used in the Post-Game section to determine whether the current player has completed his moves. It's also used in the isdoublemove subroutine to evaluate whether a piece can move again and in certain functions to change their values when a piece makes the second part of a double move. Making multiple moves works by letting the player make a single move first, showing the new position, and then letting the same player make another move. So, the moves to be evaluated might be all the moves made in a turn or just the first part of a move, and the code needs to tell the difference.
The firstcapture variable is set to true when a piece makes a capture on the first part of a double move, and false otherwise. This variable is used to help enforce the promotion rules, particularly the rule against promoting after skipping a turn.
In the fourth line here, it makes the first move stored in the mvs array. When written in the code, moves are normally preceded with "MOVE:". This line creates a string consisting of "MOVE: " concatenated with the move string, then uses eval to execute the string as though it were a line of code.
Now we get to the first section of the Post-Move code that tests a move's legality after it has been made. The first few lines ensure Black hasn't captured one of his own pieces.
if match old #bpieces; die You may not capture your own pieces.; endif;
The next two lines store the values origin and dest in case they change. These are needed for rule enforcement and for certain piece functions.
set ori origin; set dst dest;
The next section of code tests for promotions. Note that it will not execute if the isdoublemove subroutine returns true. This is done to prevent promotions from obstructing the second part of a move.
if not sub isdoublemove origin dest and sub bpromotion #ori dest: inc i; if < var i count var mvs: set ori2 origin; set dst2 dest; eval join "MOVE: " trim elem var i mvs; if != var dst2 dest or != var ori2 origin or == pass trim elem var i mvs: die Only a promotion can immediately follow a promotable piece moving to or from the promotion zone.; endif; endif; endif;
The first line of this section calls the bpromotion subroutine, which looks like this:
sub bpromotion from to: if onboard where #from 0 #pzs and onboard where #to 0 #pzs: return false; elseif not match moved p gb d n i c s bt fl g de rv l ky ph sm vm b ss vs r dh dk cs hf se ln q bg rg wb: return false; else: return true; endif; endsub;
This subroutine simply tests whether a move entitles a piece to promote and returns the corresponding value.
The next section of code tests the legality of the move itself.
set legal false; switch moved: case p p$ d d$ n n$ i i$ c c$ s s$ bt bt$ fl fl$ g g$ de de$ rc rc$ l l$ ky ky$ ph ph$ sm sm$ vm vm$ b b$ ss ss$ vs vs$ r r$ dh dh$ dk dk$ cs cs$ hf hf$ se se$ ln ln$ fk fk$ bg bg$ rg rg$ wb wb$ fe lh vg gg fd k: gosub bpromote #ori dest; break; default: die You may not move a moved; endswitch; if not var legal; die You may not move a moved from origin to dest; elseif match moved +cs and fn k origin dest: empty dest; add moved origin; endif;
It's extremely important to note that the bpieces variable is not used when evaluating moves with the switch statement. That's because the switch statement interprets #bpieces and var bpieces as strings, not as variables.
The last part of this section is responsible for moving a Heavenly Tetrarch back to its origin square after it captures something on an adjacent square. This ensures that the player doesn't need to enter the second part of the piece's igui capture. A similar block of code is used later on to handle the Fire Demon's burning moves.
When evaluating a move's legality, this subroutine calls the bpromote subroutine in the Pre-Game section, which tests a move's legality and handles promotions. It looks like this:
sub bpromote from to; set legal false; verify fn moved origin dest; if != space #to moved and sub isdoublemove origin dest: die "You may not promote any piece without completing your move."; elseif != space #to moved and onboard where #from 0 #pzs and onboard where #to 0 #pzs: die "You may not promote any piece without entering or exiting the promotion zone."; elseif not onboard where #from 0 #pzs or not onboard where #to 0 #pzs: if == moved space #to and not fnmatch *-dest* thismove and match moved p gb d n i c s bt fl g de rv l ky ph sm vm b ss vs r dh dk cs hf se ln q bg rg wb and not sub isdoublemove origin dest: set pc moved; set pm elem moved promote; askpromote #pm #pc; elseif != moved space dest and != elem moved promote space dest: set np space #to; die "You may not promote a" moved "to a" #np; endif; endif; set legal true; return true; endsub;
The first thing this subroutine does is set the legal variable to false. Then test the move's legality with verify fn moved origin dest. This statement exits the subroutine early if the moved piece did not move according to its function, which allows the Post-Game code to catch illegal moves. The rest of this subroutine handles promotions. The first few lines ensure that the promotion move isn't made prematurely or outside the promotion zone. The promotion zone is defined by the pzs variable, which is set to 5 in this case. If all of these conditions are satisfied, the subroutine will handle the promotion moves with the askpromote operator. The last section in the the code for handling promotions simply ensures that a piece did not make an illegal promotion. If no illegal moves were made, the subroutine sets legal to true and returns true.
The next few lines set the value of the firstcapture variable to true if a capture was made.
if capture: set firstcapture true; endif;
Now we get to the part of the code for handling the second part of a double move, which looks like this:
if var firstpart and sub isdoublemove origin dest: inc i; do while < var i count var mvs: set firstpart false; set mv trim elem var i mvs; if == var mv pass: if not sub isdoublemove origin dest and sub bpromotion #ori dest: inc i; if < var i count var mvs: set ori2 origin; set dst2 dest; eval join "MOVE: " trim elem var i mvs; if != var dst2 dest or != var ori2 origin: die Only a promotion can immediately follow a promotable piece moving to or from the promotion zone.; endif; endif; endif; gosub bpromote #ori dest; break; endif; eval join "MOVE: " var mv; if match old #bpieces; die You may not capture your own pieces.; elseif != var dst origin: die You may not move more than one piece per turn.; endif; if not sub isdoublemove origin dest and sub bpromotion #ori dest: inc i; if < var i count var mvs: set ori2 origin; set dst2 dest; eval join "MOVE: " trim elem var i mvs; if != var dst2 dest or != var ori2 origin: die Only a promotion can immediately follow a promotable piece moving to or from the promotion zone.; endif; endif; endif; set legal false; switch moved: case hf +dh se +dk ln +ky fe +q lh +ln vg +bg fd +wb: gosub bpromote #ori dest; break; default: die You may not move a moved; endswitch; if not var legal; die You may not move a moved from origin to dest; elseif match moved fd +wb and fn k origin dest and != #ori dest: empty dest; add moved origin; endif; loop never; endif;
Thie first section increments
and executes code within a do-while loop that executes only once if the right criteria are met. When a loop ends with loop never, it functions similarly to an if block. This is done to allow the use of the break statement, which will move execution to the end of a loop but not to the end of an if block. THe loop will only execute if the isdoublemove subroutine returns true. If the subroutine returns false, then the move is a normal move and is handled accordingly.
The first thing the loop does is handle the pass keyword, which is used to skip the second part of a double move. This keyword is always allowed, since a piece is never required to make the second part of a double move. The code then tests for promotions, calls the bpromote subroutine to handle them, and exits the loop early with the break statement.
If the pass keyword was not entered into the Moves field, then the move is a normal move, and the remaining code inside the loop executes instead. This section is similar to the code for evaluating the first part of a move. It tests for promotions, and then tests the move itself and handles promotions. The main difference is that the list of pieces in the switch statement is restricted to those that can make double moves, since it doesn't make sense to include the other pieces here.
The remaining code is used for housekeeping:
if != #ori dest: set bskips 0; elseif != space dest moved: set bskips 0; elseif var firstcapture: set bskips 0; else: inc bskips; endif; inc i; if < var i count var mvs: die Too many moves.; endif; if not sub isdoublemove origin dest: set posvar join "b" join fencode boardflags; inc #posvar; endif;
The first section keeps track of how many consecutive times a player has skipped a turn to enforce the rule against perpetually skipping a turn. It increments the bskips variable if the move skipped a turn and sets it to 0 otherwise. The second section prevents players from making more moves than are allowed. The last section keeps track of repeated board positions for enforcing fourfold repetition.
Now we get to the Post-Game code, which is used to test for winning conditions and to display legal moves. It looks like this:
if >= #bskips 1 and >= #wskips 1: die You may not skip a turn when the immediately preceding turn was skipped by the opponent.; endif; if not findpiece K spaces and not findpiece +DE spaces: say All Royals Captured! Black has won!; won; endif; set posvar join "b" join fencode boardflags; if >= var #posvar 4 and < #bskips 2: say Four Times Repetition! Drawn Game!; drawn; endif; if var firstpart and sub isdoublemove origin dest and match moved hf +dh se +dk ln +ky fe +q lh +ln vg +bg fd +wb: remind "Enter second leg of move or write 'pass' in the Moves field"; gosub legalmoves2 king; continuemove; elseif sub stalemated KING: endif;
The first few lines of code make up the only rule-enforcing code in the Post-Game section. They exit with an error message if the player skipped a turn when the last turn was skipped by the opponent. The next few lines test if the opponent has a King and/or Prince on the board and exits with a win otherwise. The next section of code exits with a draw when fourfold repetition occurs.
The remainder of the code in the Post-Game sections is for displaying legal moves. The first section determines whether a piece can move again. If it can, the legalmoves2 subroutine is called, the move is continued with continuemove, and the appropriate message is displayed using remind (not say, otherwise Game Courier won't recognise the second move). Otherwise, the stalemated subroutine is called.
Note that the code passes the word king or to the stalemated subroutine instead of the usual #k. Because the code only tests if there is a King or Prince on the board, having such a variable as a parameter is unnecessary. For the other Post-Game section the word KING is used instead.
Although the example here was a variant with double-move pieces, the principles behind them can be extended to games with pieces that can move more than twice per turn. The code would get much more complicated the farther out you go, but it is absolutely possible, at least in theory.
This 'user submitted' page is a collaboration between the posting user and the Chess Variant Pages. Registered contributors to the Chess Variant Pages have the ability to post their own works, subject to review and editing by the Chess Variant Pages Editorial Staff.
By Adam DeWitt.
Last revised by Adam DeWitt.
Web page created: 2020-07-21. Web page last updated: 2020-07-24