12. Castling and En passant
Table of Contents generated with DocToc
The material used in this chapter can be downloaded at here.
In this chapter, we implement castling and En passant rules.
Castling | En passant |
---|---|
On the last chapter, we introduced a concept of object to store LMTN for each chess cells. We created the lastMovedTurn
member, but we haven't stored proper values here. To use this field, we first have to put a correct number to lastMovedTurn
member.
LMTN, by definition, is the turn count when the piece have moved the last. To put proper LMTN value there, we should
- Count 'turn number' properly.
- Store LMTN for each piece moves.
The code in afterTriggerExec
implements piece moving. Since we should update turn number by every piece moves, afterTriggerExec
function would be a great place to count turn number and update LMTNs.
/* Skipping */
var beforeUnitPlayer, beforeUnitType;
var beforeUnitX, beforeUnitY = -1, -1;
//// NEW
var currentTurn = 1; // New
//// NEW
function afterTriggerExec() {
const unitType, selX, selY = sel.getSelectedUnit();
if (unitType != -1) {
if(unitType == $U('Zerg Scourge')) {
piece.removePiece(selX, selY);
piece.placePiece(selX, selY, beforeUnitType, beforeUnitPlayer);
piece.removePiece(beforeUnitX, beforeUnitY);
RemoveUnit('Zerg Scourge', Force3);
//// NEW
tile.tile(selX, selY).lastMovedTurn = currentTurn;
currentTurn++;
//// NEW
beforeUnitX = -1;
beforeUnitY = -1;
}}} /* Skipping */
Put simply, we created currentTurn
variable to store the current turn number, and as we place the pieces we update their lastMovedTurn number. The expression tile.tile(selX, selY).lastMovedTurn = currentTurn;
may seem complex, but you can paraphrase this expression to this code.
// Create a piece on (selX, selY)
const piece = tile.tile(selX, selY); // Get the created piece object.
piece.lastMovedTurn = currentTurn; // Set its LMTN to currentTurn
So we have valid LMTN value for each pieces. Let's jump on implementing castling.
To perform a castling, we move the king pieces by two cells right or left, and then we move the rook right next to the king, on the opposite direction the king has moved to. So it's the king that first moves on the castling. So wee need to modify king.eps to perform castling. king.eps currently looks like this.
function getPossibleDestination(player, x, y) {
const dxTable = [-1, 0, 1, -1, 1, -1, 0, 1];
const dyTable = [-1, -1, -1, 0, 0, 1, 1, 1];
for(var direction = 0 ; direction < 8 ; direction++) {
const dx, dy = dxTable[direction], dyTable[direction];
if (common.canPieceGoTo(player, x + dx, y + dy)) {
common.placeScourge(player, x + dx, y + dy);
}
}
}
Remember three conditions for castling?
- Both the king piece and the rook piece shouldn't have moved before.
- There are no pieces between two pieces.
- King is not currently in check, and king won't be checked after the castling.
- Any squares the king would pass during the castling is not being attacked by opponent
We won't think about the third condition for now. Just check the first and the second one. The first condition can be checked with if (kingPiece.lastMovedTurn == 0 && rookPiece.lastMovedTurn == 0)
, where const kingPiece = tile.tile(x, y);
. We can check the second condition and get what rookPiece
is with a simple for loop, like this.
// Going to the left side
for(var x1 = x - 1 ; x1 < 8 ; x1--) {
const piece = tile.tile(x1, y);
if(piece) {
if(x1 == 0 && piece.unitType == $U('Rook') && piece.player == player) {
// rookPiece is piece.
break;
}
else break; // Something is between rook and king, or rook is not at x1=0.
}
}
// Going to the right side
for(var x1 = x + 1 ; x1 < 8 ; x1++) {
const piece = tile.tile(x1, y);
if(piece) {
if(x1 == 7 && piece.unitType == $U('Rook') && piece.player == player) {
// rookPiece is piece.
break;
}
else break; // Something is between rook and king, or rook is not at x1=7.
}
}
Two for loops look similar, so we merge them.
// Going to the left side
const dxTable = [-1, 1]; // dx
const rookXTable = [0, 7]; // corresponding rook's expected column.
for(var i = 0 ; i < 2 ; i++) {
const dx = dxTable[i];
for(var x1 = x + dx ; x1 < 8 ; x1 += dx) {
const piece = tile.tile(x1, y);
if(piece) {
if(x1 == rookXTable[i] && piece.unitType == $U('Rook') && piece.player == player) {
// rookPiece is piece.
if (piece.lastMovedTurn == 0) {
// We can perform a castling
break;
}
}
else break; // Something is between rook and king, or rook is not at x1=0.
}
}
}
To summarize, king.eps now looks like this.
import tile;
import loc;
import pieceRule.common;
function getPossibleDestination(player, x, y) {
const dxTable = [-1, 0, 1, -1, 1, -1, 0, 1];
const dyTable = [-1, -1, -1, 0, 0, 1, 1, 1];
for(var direction = 0 ; direction < 8 ; direction++) {
const dx, dy = dxTable[direction], dyTable[direction];
if (common.canPieceGoTo(player, x + dx, y + dy)) {
common.placeScourge(player, x + dx, y + dy);
}
}
const kingPiece = tile.tile(x, y);
if (kingPiece.lastMovedTurn == 0) {
const dxTable2 = [-1, 1]; // dx
const rookXTable = [0, 7]; // corresponding rook's expected column.
for(var i = 0 ; i < 2 ; i++) {
const dx = dxTable2[i];
for(var x1 = x + dx ; x1 < 8 ; x1 += dx) {
const piece = tile.tile(x1, y);
if(piece) {
if(x1 == rookXTable[i] && piece.unitType == $U('Rook') && piece.player == player && piece.lastMovedTurn == 0) {
common.placeScourge(player, x + dx * 2, y);
}
else break;
}
}
}
}
}
Done that, we now have this. I moved the right rook back and forth to update its LMTN. We now know the directions where a king can perform castling to.
You move two pieces during the castling. Piece moving is handled on afterTriggerExec
on main.eps, and our current code only moves the selected piece. During the castling, the non-selected piece(Rook) gets moved too, so we have to modify this logic a little bit.
Basically, the afterTriggerExec
knows if player issued the castling. We'll check this with these functions.
- Currently selected piece is King.
beforeUnitType == $U('King')
- Player moved the piece two cells left or right.
- selY == beforeUnitY
- selX - beforeUnitX = ±2
After we check these conditions, we should move the castled rook. Every piece movement requires two positions: position before the movement, and position after the movement. This is relatively easy, and I think you will find it out soon.
To summarize, the code looks like this.
if(unitType == $U('Zerg Scourge')) {
piece.removePiece(selX, selY);
piece.placePiece(selX, selY, beforeUnitType, beforeUnitPlayer);
piece.removePiece(beforeUnitX, beforeUnitY);
RemoveUnit('Zerg Scourge', Force3);
////// NEW
// Special rule for castling
if(beforeUnitType == $U('King') && beforeUnitY == selY) {
const deltaX = selX - beforeUnitX; // X change of the king piece.
if(deltaX == -2 || deltaX == 2) { // Castling was performed.
// Also move rook.
const rookBeforeX = (deltaX == -2) ? 0 : 7;
const rookAfterX = selX + ((deltaX == -2) ? 1 : -1);
piece.removePiece(rookBeforeX, selY);
piece.placePiece(rookAfterX, selY, $U('Rook'), beforeUnitPlayer);
tile.tile(rookAfterX, selY).lastMovedTurn = currentTurn;
}
}
////// NEW
// We update the currentTurn after processing castling rule.
tile.tile(selX, selY).lastMovedTurn = currentTurn;
currentTurn++;
beforeUnitX = -1;
beforeUnitY = -1;
}
If you coded the logics yourself, you might encounter an 'orphan condition' error. This happens when you try to do arithmetics of condition and value. For example, if you write
const rookAfterX = selX + (deltaX == -2) ? 1 : -1;
, the expressionselX + (deltaX == -2)
is interpreted as a condition for ternary operator. In here we apparently is adding value(selX
) and condition(deltaX == -2
). You should be careful not to write this kind of code.
And now we have a castling!
En passant rule has two conditions.
- Captured pawn has just moved two squares.
- Capturing pawn is right next to the captured pawn.
These two conditions are somewhat hard to express directly, so I'll modify them a bit.
- Captured pawn is two rows front of its initial position.
- This can be checked by comparing
capturedPawn.lastMovedTurn
with
- This can be checked by comparing
- Captured pawn has just moved.
- Capturing pawn is right next to the captured pawn.
So, pawn.eps
should access currentTurn
variable defined in main.eps
. To do that, we just import main.eps in pawn.eps.
The rest of the part is quite trivial. Here's the completed code. I created an canIssueEnpassant
function for checking the mentioned condition 1 and 3.
import tile;
import loc;
import main; // Import main.eps to use currentTurn variable
import pieceRule.common;
/**
* Helper for en passant function.
*/
function canIssueEnpassant(player, x, y, dx) {
if(dx == -1 && x == 0) return false;
else if(dx == 1 && x == 7) return false;
const piece = tile.tile(x + dx, y);
return (
piece &&
piece.player != player &&
piece.lastMovedTurn == main.currentTurn - 1
);
}
function getPossibleDestination(player, x, y) {
var opponentPlayer, yStart, dy;
/** Same as before */
if (y + dy < 8) {
/** Same as before */
}
// En passant rule
var enPassantY = (player == $P7) ? 4 : 3;
if(y == enPassantY) {
if(canIssueEnpassant(player, x, y, -1)) {
common.placeScourge(player, x - 1, y + dy);
}
if(canIssueEnpassant(player, x, y, 1)) {
common.placeScourge(player, x + 1, y + dy);
}
}
}
-
Creating chess
-
Appendixes