School Java Project Chinese Chess (3)
Implementing game rules
We are still working on logical game board and not worrying about any GUI concerns like mouse clicking or dragging. In the first post of this series we got an empty game board printed out. And in the second one we got all the initial 32 pieces deployed. It’s time to make a move. Since we don’t have any GUI code yet, let’s test our code using one or two hardcoded moves and print out the result board.
One way to do this is by simply removing the piece from the starting location and adding a brand new one at the destination. Like this:
void movePiece(int fromCol, int fromRow, int toCol, int toRow) {
Piece movingP = pieceAt(fromCol, fromRow);
Piece targetP = pieceAt(toCol, toRow);
pieces.remove(movingP);
pieces.remove(targetP);
pieces.add(new Piece(toCol, toRow, movingP.isRed, movingP.rank));
}
In file CChess.java, add the above code inside CChessBoard
class below its constructor CChessBoard()
.
Now add 2 lines in the main method to see moving piece in action:
class CChess {
public static void main(String[] args) {
CChessBoard brd = new CChessBoard();
System.out.println(brd);
brd.movePiece(0, 0, 0, 2); // tmp code for testing
System.out.println(brd); // tmp code for testing
}
}
Here is the output before and after moving piece R from (0, 0) to (0, 2):
0 1 2 3 4 5 6 7 8
0 R N B G K G B N R
1 . . . . . . . . .
2 . C . . . . . C .
3 P . P . P . P . P
4 . . . . . . . . .
5 . . . . . . . . .
6 p . p . p . p . p
7 . c . . . . . c .
8 . . . . . . . . .
9 r n b g k g b n r 0 1 2 3 4 5 6 7 8
0 . N B G K G B N R
1 . . . . . . . . .
2 R C . . . . . C .
3 P . P . P . P . P
4 . . . . . . . . .
5 . . . . . . . . .
6 p . p . p . p . p
7 . c . . . . . c .
8 . . . . . . . . .
9 r n b g k g b n r
But games have rules. A piece can’t move without constraints. Let’s add the following handy helper methods for each rank in class CChessBoard
.
Here is the empty board with coordinates for convenience. Note we assume the upper is Red side and the lower is Black side:
0 1 2 3 4 5 6 7 8
0 . . . . . . . . .
1 . . . . . . . . .
2 . . . . . . . . .
3 . . . . . . . . .
4 . . . . . . . . .
5 . . . . . . . . .
6 . . . . . . . . .
7 . . . . . . . . .
8 . . . . . . . . .
9 . . . . . . . . .
Check if a piece is out of board:
private boolean outOfBoard(int col, int row) {
return col < 0 || col > 8 || row < 0 || row > 9;
}
Check if a move is straight (horizontal or vertical):
private boolean isStraight(int fromCol, int fromRow, int toCol, int toRow) {
return fromCol == toCol || fromRow == toRow;
}
Check if a move is diagonal:
private boolean isDiagonal(int fromCol, int fromRow, int toCol, int toRow) {
return Math.abs(fromCol - toCol) == Math.abs(fromRow - toRow);
}
Get step length. King, Guard and Pawn have step length 1. Bishop has step length 2. We won’t use this method for Knight’s move.
private int steps(int fromCol, int fromRow, int toCol, int toRow) {
if (fromCol == toCol) {
return Math.abs(fromRow - toRow);
} else if (fromRow == toRow) {
return Math.abs(fromCol - toCol);
} else if (isDiagonal(fromCol, fromRow, toCol, toRow)) {
return Math.abs(fromRow - toRow);
}
return 0; // neither straight nor diagonal
}
Check if a piece is out of palace:
private boolean outOfPalace(int col, int row, boolean isRed) {
if (isRed) {
return col < 3 || col > 5 || row < 0 || row > 2;
} else {
return col < 3 || col > 5 || row < 7 || row > 9;
}
}
Check if a piece is on self side:
private boolean selfSide(int row, boolean isRed) {
return isRed ? row <= 4 : row >= 5;
}
Count how many pieces there are between 2 locations. Simply return 0 if the two points are neither horizontal nor vertical aligned.
private int numPiecesBetween(int fromCol, int fromRow,
int toCol, int toRow) {
if (!isStraight(fromCol, fromRow, toCol, toRow)
|| steps(fromCol, fromRow, toCol, toRow) < 2) {
return 0;
}
int cnt = 0, head, tail;
if (fromCol == toCol) { // vertical
head = Math.min(fromRow, toRow);
tail = Math.max(fromRow, toRow);
for (int row = head + 1; row < tail; row++) {
cnt += (pieceAt(fromCol, row) == null) ? 0 : 1;
}
} else {
head = Math.min(fromCol, toCol);
tail = Math.max(fromCol, toCol);
for (int col = head + 1; col < tail; col++) {
cnt += (pieceAt(col, fromRow) == null) ? 0 : 1;
}
}
return cnt;
}
Check if it is a self killing:
private boolean selfKilling(int fromCol, int fromRow,
int toCol, int toRow, boolean isRed) {
Piece target = pieceAt(toCol, toRow);
return target != null && target.isRed == isRed;
}
Check if it is a valid Guard move. Guard is limited inside palace and can only go diagonally, one step in each move:
private boolean isValidGuardMove(int fromCol, int fromRow,
int toCol, int toRow, boolean isRed) {
if (outOfPalace(toCol, toRow, isRed)) {
return false;
}
return isDiagonal(fromCol, fromRow, toCol, toRow) &&
steps(fromCol, fromRow, toCol, toRow) == 1;
}
Check if it is a valid King move. King is limited inside palace and can only go straightly (horizontally or vertically), one step in each move:
private boolean isValidKingMove(int fromCol, int fromRow,
int toCol, int toRow, boolean isRed) {
if (outOfPalace(toCol, toRow, isRed)) {
return false;
}
return isStraight(fromCol, fromRow, toCol, toRow) &&
steps(fromCol, fromRow, toCol, toRow) == 1;
}
Check if it is a valid Knight move. From a specific location, Knight may have 8 possible destinations to jump onto, if it is not blocked by those 4 blockers (shown in blue circle below).
private boolean isValidKnightMove(int fromCol, int fromRow,
int toCol, int toRow) {
if (Math.abs(fromCol - toCol) == 1 && Math.abs(fromRow - toRow) == 2) {
return pieceAt(fromCol, (fromRow + toRow)/2) == null;
} else if (Math.abs(fromCol - toCol) == 2 && Math.abs(fromRow - toRow) == 1) {
return pieceAt((fromCol + toCol)/2, fromRow) == null;
}
return false;
}
Check if it is a valid Bishop move. Bishop has to stay on self side and needs to check blocker.
private boolean isValidBishopMove(int fromCol, int fromRow,
int toCol, int toRow, boolean isRed) {
return selfSide(toRow, isRed)
&& pieceAt((fromCol + toCol)/2, (fromRow + toRow)/2) == null
&& isDiagonal(fromCol, fromRow, toCol, toRow)
&& steps(fromCol, fromRow, toCol, toRow) == 2;
}
Check if it is a valid Rook move:
private boolean isValidRookMove(int fromCol, int fromRow,
int toCol, int toRow) {
return isStraight(fromCol, fromRow, toCol, toRow)
&& numPiecesBetween(fromCol, fromRow, toCol, toRow) == 0;
}
Check if it is a valid Cannon move:
private boolean isValidCannonMove(int fromCol, int fromRow,
int toCol, int toRow) {
if (pieceAt(toCol, toRow) == null) {
return isValidRookMove(fromCol, fromRow, toCol, toRow);
}
return numPiecesBetween(fromCol, fromRow, toCol, toRow) == 1;
}
Check if it is a valid Pawn move:
private boolean isValidPawnMove(int fromCol, int fromRow,
int toCol, int toRow, boolean isRed) {
if (steps(fromCol, fromRow, toCol, toRow) != 1) {
return false;
}
return isRed && toRow > fromRow || !isRed && toRow < fromRow || !selfSide(fromRow, isRed);
}
Finally we can check the move of any piece:
boolean validMove(int fromC, int fromR, int toC, int toR) {
if (fromC == toC && fromR == toR || outOfBoard(toC, toR)) {
return false;
}
Piece p = pieceAt(fromC, fromR);
if (p == null || selfKilling(fromC, fromR, toC, toR, p.isRed)) {
return false;
}
boolean ok = false;
switch (p.rank) {
case GUARD:
ok = isValidGuardMove(fromC, fromR, toC, toR, p.isRed);
break;
case KING:
ok = isValidKingMove(fromC, fromR, toC, toR, p.isRed);
break;
case BISHOP:
ok = isValidBishopMove(fromC, fromR, toC, toR, p.isRed);
break;
case KNIGHT:
ok = isValidKnightMove(fromC, fromR, toC, toR);
break;
case ROOK:
ok = isValidRookMove(fromC, fromR, toC, toR);
break;
case CANNON:
ok = isValidCannonMove(fromC, fromR, toC, toR);
break;
case PAWN:
ok = isValidPawnMove(fromC, fromR, toC, toR, p.isRed);
break;
}
return ok;
}
We’ll use the above method to control how to move a piece when we finish the GUI part of this app in our future posts. Until then, here is an easy way to test all piece rules (we use rook as an example below) we wrote in this post:
class CChess {
public static void main(String[] args) {
CChessBoard brd = new CChessBoard();
System.out.println(brd); // tmp code for testing
System.out.println(brd.validMove(0, 0, 1, 1)); // false
System.out.println(brd.validMove(0, 0, 0, 2)); // true
}
}
Next, we’ll enter the wonderful world of GUI, Graphical User Interface.
These programming concepts were used in this post:
algorithm
Think of it as our plan about how to implement a specific logic, or how to solve a specific problem, e.g., to check if a move of rook is valid or not. We may have good plans or bad ones, i.e. good algorithms or bad ones.
refactoring
Restructuring existing code without changing its behaviour. See below “Code snippets that can be improved” for examples of refactoring in action.
comments
// comments out a single line, or part of a single line
/* … */ comments out a block of text
/** … */ has something to do with documentation called javadoc.
Code snippets that can be improved:
Redundant “== true”
private boolean outOfPalace(int col, int row, boolean isRed) {
if (isRed == true) {
return col < 3 || col > 5 || row < 0 || row > 2;
} else {
return col < 3 || col > 5 || row < 7 || row > 9;
}
}
Because isRed
has type boolean
there is no need of == true
. Just like the daily life speaking, we say “If it rains, blah blah blah …” and we don’t say “If it-rains is true, blah blah blah …”.
if something then return true, otherwise return false
In this case simply return that “something” directly. So instead of:
private boolean isStraight(int fromCol, int fromRow, int toCol, int toRow) {
if (return fromCol == toCol || fromRow == toRow) {
return true;
} else {
return false;
}
}
let’s use:
private boolean isStraight(int fromCol, int fromRow, int toCol, int toRow) {
return fromCol == toCol || fromRow == toRow;
}