How to Program Piece Drops for 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)
This tutorial is based on code from the Chessgi preset and the chessgi include file. I chose this to illustrate drops, because Chessgi is closer to Chess than Shogi is, and the previous tutorials have focused on programming Chess. But there is one caveat. The code for Chessgi is based on different code for Chess than what was covered in the tutorials. This code is more optimized for Chess, making it less suitable for adaption to games with non-standard pieces. For an example of Chess programmed more along the same lines as Chessgi is here, I refer you to this Chesspreset and the chess include file.
In games like Shogi and Chessgi, captured pieces become the pieces of the player who captured them, and he may use a later turn to drop a captured piece on the board. The first thing this involves is moving a captured piece to an off-board location. This area should be allocated ahead of time by including extra ranks in the board. The rank labels for these ranks should begin with an exclamation point, which is the signal for not displaying a rank label. You should also include an empty rank on each side of the board to separate the regular playing area from the storage areas. An empty rank may use the null string as a rank marker. Since rank labels should be separated by spaces, placing two spaces together with nothing between them counts as a null string. See the presets for Shogi or Chessgi for examples.
When capturing a piece, you will want to move it to storage area for that player. Here is how it is done in Chessgi when White captures a Black piece:
if != old nil: set temp dest; drop old last !A8 !A7 !A6 !A5 !A4 !A3 !A2 !A1 !B8 !B7 !B6 !B5 !B4 !B3 !B2 !B1 !C8 !C7 !C6 !C5 !C4 !C3 !C2 !C1 !D8 !D7 !D6 !D5 !D4 !D3 !D2 !D1; flip dest; setglobal "dest" #temp; endif;
old identifies the piece that used to be on the space just moved to. If a piece was captured, it needs to be moved. This will be done with the
drop command, which will place the piece on the last empty location it comes to in the list of spaces following it. If the
last keyword were not there, it would drop it on the first. This is what Black will do when it captures a White piece, so that the pieces each side captures will go to opposite ends of the same area. If the code didn't use the same area for both sides, it would have to allocate more ranks for each side to cover all the pieces each side could potentially have in hand. Before it uses this command, though, it has to account for a side effect of the command. This command will update the value of
dest, which is used to store the location of the last space moved to. But the space stored in
dest should remain its current value. So this gets saved to a temporary variable, and when everything is done,
setglobal is used to restore
dest to its original value. One more thing to do after moving the piece to off-board storage is to change its side. This is done with the
flip command, which makes use of the value of
dest set by the
Checking the legality of a drop does not work the same as checking the legality of a regular move. Since GAME Code is not an object-oriented language, a piece is not an object whose powers of movement must all be coded together. Here is the extra post-move code that is used for enforcing legal piece drops by White in Chessgi:
if not isalnum origin: if capture: die You may not drop a piece on an occupied space.; elseif and equal moved P equal rankname dest 8: die You may not drop a Pawn on your last rank.; elseif unequal space dest moved: die You may not promote a Pawn while dropping it.; elseif not isalnum dest: die You may not drop a piece off the board.; endif; set legal true; elseif ...
origin provides the coordinate of the space moved from. A drop will be done with the * operator, which includes the piece label and the destination. Despite not being part of the notation, the coordinate the piece is moved from is still made available through
origin. Behind the scenes, this operator works by searching an array of spaces called $starpath for the first player or $starpath for the second. $starpath is a two-dimensional array that can be set explicitly with
setglobal, but the job of populating this array is being handled here by the
drop command. When the first argument of this command matches the piece last captured, and the $starpath array for the current player hasn't already been set, it populates it with the coordinates given to it as arguments.
Remember that I made the rank labels for the off-board storage areas invisible by beginning them with an exclamation mark. This means that the coordinates in these areas are not fully alphanumeric. So, if a piece is moved from a space whose coordinate is not alphanumeric, this code identifies it as a drop move. Since a drop must be to an empty space, it first confirms that the move was not a capture. It then makes sure that a Pawn wasn't dropped on the last rank, that a dropped piece wasn't additionally promoted, and that the move was actually to the board. The
elseif at the end goes into the code for enforcing regular legal moves.
Finally, potential drops have to be handled. For some presets, this is handled entirely by the
stalemated subroutine. But Chessgi is based on code that uses both
stalemated subroutines. So we'll look at what is going on in each of these. Let's start with the
stalemated subroutine, since some presets use this alone, identifying checkmate as simply check + stalemate. Actually, let me backtrack and qualify this. Since checkmate is checked for with a different subroutine, this version of the
stalemated subroutine is optimized for positions in which the King is not in check, and that makes a crucial difference in how drops are handled. Where the King is not in check, all possible drops will be legal, but when the King is in check, only drops that block the check will be legal. It is more efficient to use both
stalemated subroutines, because each can do its job more efficiently when it knows from the start whether the King is in check. Using just a
stalemated subroutine is suitable for games with Cannons or other unusually moving pieces, but when a game includes drops, using separate
stalemated subroutines will improve efficiency even more than usual. With that in mind, here is the subroutine used for Chessgi with relevant sections highlighted:
// May be changed for games with different Pawn drop rules than Chessgi set pawndropfunc lambda (#0 onlyif onboard where #0 0 var forward); sub stalemated king; local legalmove temp from piece to attacked ra; if isupper space #king: def friend isupper #0; def friends onlyupper; set attacked ATTACKEDBYB; set cspaces var wcastle; set pawn P; set forward 1; else: def friend islower #0; def friends onlylower; set attacked ATTACKEDBYW; set cspaces var bcastle; set pawn p; set forward -1; endif; store; // Find Legal King Moves set kingmoves fn KL #king; for to #kingmoves: if not fn friend space #to and onboard #to: move #king #to; set incheck fn var attacked #to; restore; if not #incheck: setlegal #king #to; endif; endif; next; for to var cspaces: if sub castlepos #king #to: set incheck fn var attacked #to; restore; if not #incheck: setlegal #king #to; endif; endif; next; // Calculates legal drop spaces set piecedrops aggregate lambda (#0 onlyif and empty #0 isalnum #0) spaces; set pawndrops aggregate var pawndropfunc #piecedrops; // Can another piece legally move? restore; for (from piece) fn friends: if == #from #king: continue; endif; if isalnum #from: for to fn join #piece L #from: if fn #piece #from #to and not fn friend space #to and onboard #to: move #from #to; set incheck fn var attacked #king; if not #incheck: setlegal #from #to; endif; endif; restore; next; elseif == #piece #pawn: for to #pawndrops: setlegal #from #to; next; else: for to #piecedrops: setlegal #from #to; next; endif; next; // All done. Return true if there are no legal moves. return cond count system legalmoves false true; endsub;
The first thing it does with regard to piece drops is calculate arrays of spaces pieces may be dropped to. It does this with the
aggregate function, which produces an array of all non-empty values passed by a lambda function. The lambda function is enclosed in parentheses, and it is executed for each space on the board.
#0 represents the space, and if that space is empty and alphanumeric (meaning it is on the board), its value gets returned by the lambda function. Since Pawns may not be dropped onto the last rank, it then uses
aggregate again to create an array of legal destinations for Pawn drops. This time it uses a lambda function defined in the variable
pawndropfunc. By using the output of the previous function as its input, it doesn't have to check whether the space is empty or on the board. What it checks for is whether there is a space in front of the space. If there is, it is not the last rank, and a Pawn may be dropped there. Since each side has a different forward direction, it uses the value of
forward set earlier in the
stalemated subroutine, which is determined by checking whether the King on the space passed to it is uppercase. The
pawndropfunc is used to define this second lambda function to make modification for other games easier. For example, Crazyhouse can use the same include file as Chessgi simply by changing the value of this variable. Since Crazyhouse doesn't allow drops on the first or last rank, the lambda function it uses looks like this:
set pawndropfunc lambda (#0 onlyif and onboard where #0 0 1 onboard where #0 0 -1);
Since it is checking that there is a space both in front of and behind the space, it doesn't need to rely on the value of
forward, as Chessgi does.
Now, making an array of spaces for possible drops is only preliminary to finding legal drops. Later in the code, it loops through all the pieces in play. For pieces on the board, it checks for regular legal moves, and for pieces off the board, it calculates legal drops. Since the work of testing the spaces has already been done earlier, all it has to do to calculate legal drops is to loop through the appropriate array and assign a legal move to every space in the array. It picks the right array simply by testing whether the piece to drop is a Pawn. Although assigning legal moves is not necessary for checking whether there is a single legal move, which is all the
stalemated subroutine originally did, it is necessary for displaying legal moves when a player clicks on a piece. It just happened to be more efficient to let the
stalemated subroutine, which was already being used anyway, handle this job than to have a new subroutine handle it.
Let's now take a look at the
checkmated subroutine. The relevant difference between this and the
stalemated subroutine is that when the King is in check, the only legal drops will be those that block a check. Here is the code with relevant sections highlighted:
sub checkmated king checks; // Is the King in check at all? verify checks; store; if isupper space #king: def friends onlyupper; def friend isupper #0; set attacked ATTACKEDBYB; set pat [A-NQ-Z]; set forward 1; set pawn P; else: def friends onlylower; def friend islower #0; set attacked ATTACKEDBYW; set pat [a-nq-z]; set forward -1; set pawn p; endif; // Is there only one checking piece? // Two checking pieces cannot both be blocked or captured. if == count var checks 1: // Can the check be captured or blocked without revealing another check? // Loops once through single element of array. key is coordinate, val is piece. for (key enemy) var checks: set possible path #king #key; set piecedrops var possible; set pawndrops aggregate var pawndropfunc #piecedrops; push possible #key; // Is the checking piece a Pawn that may be captured by en passant? // If so, push the space it passed over onto the possible array. if == #key #ep: push possible cond isupper space #ep where #ep 0 -1 where #ep 0 1; endif; for (from piece) fn friends: if == #from #king: continue; elseif isalnum #from: for to #possible: if fn #piece #from #to: move #from #to; set incheck fn var attacked #king; if not #incheck: setlegal #from #to; endif; endif; restore; next; elseif == #piece #pawn: for to #pawndrops: setlegal #from #to; next; else: for to #piecedrops: setlegal #from #to; next; endif; next; next; endif; // Find Legal King Moves set kingmoves fn KL #king; for to #kingmoves: if not fn friend space #to and onboard #to: move #king #to; set incheck fn var attacked #to; restore; if not #incheck: setlegal #king #to; endif; endif; next; // All done. Set $legalmoves and return; return cond count system legalmoves false true; endsub;
Since drops will be legal only if they block a check, and blocking a check will be possible only if the King is checked by no more than one piece, it first checks for that. So long as only one piece is checking the King, it first creates an array of the spaces between the King and the checking piece and copies this to the
piecedrops variable. Using this variable as input, it then calculates the
pawndrops variable as it did in the
stalemated routine. When calculating legal drops, it does exactly the same thing as the
stalemated subroutine did. As long as the King is checked by a single piece, any otherwise legal drop to a space between the King and the checking piece will be legal.
The same methods described above also work well for Shogi. See the Shogi preset and its shogi include file for details. The main differences to pay attention to are that promoted Shogi pieces are demoted when captured, there are different types of pieces than in Chess, and there are some different restrictions on dropping different pieces than in Chessgi.
Written by Fergus Duniho
WWW Page Created: 02 May 2016