Java初尝试:国际象棋
本文使用Java实现一个在终端中输出的基础国际象棋程序。从UML图出发,剖析游戏中对象的属性、方法和不同对象之间的关系,以此理解面向对象的编程思想。最后将设计转化为Java代码的最终实现。

统一建模语言UML(Unified Modeling Language)是面向对象设计的建模工具,独立于任何具体程序设计语言。从图中,我们可以清晰的看到对象所包含的属性和方法,以及输入输出变量类型、静态与否、访问权限等。更重要的是,它还体现了不同对象之间的关系,包含关联、聚合、组合、继承等,为辅助和理解面向对象设计提供了极大的帮助。
下面依次梳理UML图中的各个对象以及它们之间的关系。
首先Game类是整个游戏的驱动器,也是程序运行的起点,该类中的属性和方法均为静态。main()方法承载了游戏界面、棋盘和棋子的初始化工作,并调用play()方法,即游戏循环的主体,负责处理用户输入、走子、轮次切换、投降、胜利检测等。(不涉及平局判断、将军判断、计时等)
在处理用户输入时,需要验证用户输入的合法性,该验证工作由CheckInput类完成,其提供checkCoordinateValidity()方法验证输入字符串并返回验证结果的布尔值。其中,用户的输入必须为由数字1-8和字母a-h表示的两位的坐标。
棋盘作为游戏的载体是不可或缺且唯一的,每一局游戏都存在一个棋盘,且棋盘不能脱离游戏而存在。因此Board类和Game类之间形成一对一的组合关系。棋盘类中提供了必要的用于初始化和获取或设置棋盘元素的方法,包括printBoard()打印棋盘,movePiece()移动棋子等等。这些方法都应该是公开可见的静态方法供Game驱动器调用。
国际象棋中的棋盘大小为8×8,在程序中用一个8×8的二维Square数组表示。棋盘上每一个位置的方格被封装成Square类,其不能脱离棋盘存在,与棋盘同样构成组合关系(8×8对1)。Square类中包含内部属性有是否有子hasPiece,所含棋子Piece,这些内部属性由公共可见的几个setter和getter方法来被安全的读写。
在具体考虑具体的棋子之前,我们可以把已经分析出的整体游戏结构实现为Java代码。注意:所有的代码文件应处于包chess中。
package chess;
import java.io.Console;
public class Game {
private static boolean gameEnd = false;
// This method requires your input
public static void play() {
Console keyboardConsole = System.console();
String userInput;
CheckInput inputChecker = new CheckInput();
int[] oriPos = new int[2];
int[] desPos = new int[2];
// start from WHITE
PieceColour playerTurn = PieceColour.WHITE;
while (!gameEnd) {
// print current turn (White or Black)
System.out.println("\n------ " + (playerTurn == PieceColour.WHITE ? "Whites" : "Blacks") + " move -------");
// handle user input: origin
System.out.println("> Enter origin:");
userInput = keyboardConsole.readLine();
// check END command (resign)
if (userInput.trim().equals("END")) {
System.out.println("! " + (playerTurn == PieceColour.WHITE ? "White" : "Black") + " resigns.");
break;
}
userInput = inputChecker.handleUserInput(userInput);
if (userInput == null) {
continue;
}
oriPos[0] = userInput.charAt(0) - '0' - 1;
oriPos[1] = userInput.charAt(1) - 'a';
// check validility of origin pos
if (!(Board.hasPiece(oriPos[0], oriPos[1]) && Board.getPiece(oriPos[0], oriPos[1]).getColour() == playerTurn)) {
System.out.println("* Invalid original coordinates. Only allowed to move pieces of your color. Please try again.");
continue;
}
// handle user input: destination
System.out.println("> Enter destination:");
userInput = keyboardConsole.readLine();
// check END command (resign)
if (userInput.trim().equals("END")) {
System.out.println("! " + (playerTurn == PieceColour.WHITE ? "White" : "Black") + " resigns.");
break;
}
userInput = inputChecker.handleUserInput(userInput);
if (userInput == null) {
continue;
}
desPos[0] = userInput.charAt(0) - '0' - 1;
desPos[1] = userInput.charAt(1) - 'a';
// check whether the intended movement is legitimate
if (!Board.getPiece(oriPos[0], oriPos[1]).isLegitMove(oriPos[0], oriPos[1], desPos[0], desPos[1])) {
System.out.println("* Illegal movement. Please try again.");
continue;
}
// move piece
gameEnd = Board.movePiece(oriPos[0], oriPos[1], desPos[0], desPos[1], Board.getPiece(oriPos[0], oriPos[1]));
// print board
Board.printBoard();
// switch turn
playerTurn = (playerTurn == PieceColour.WHITE ? PieceColour.BLACK : PieceColour.WHITE);
}
// print winnner
System.out.println("\n------ Game Over -------");
System.out.println("------ " + (playerTurn == PieceColour.WHITE ? "Black" : "White") + " Wins! -------");
}
// This method should not be edited
public static void main(String args[]) {
Board.initialiseBoard();
Board.initialisePieces();
Board.printBoard();
Game.play();
}
}
package chess;
public class CheckInput {
// This method requires your input
public boolean checkCoordinateValidity(String input) {
return input.length() == 2 && input.charAt(0) >= '1' && input.charAt(0) <= '8' && input.charAt(1) >= 'a' && input.charAt(1) <= 'h';
}
// custom added method
public String handleUserInput(String input) {
// check validity
if (!checkCoordinateValidity(input)) {
// try to guess correct user input
input = input.trim().replace(" ", "").toLowerCase();
if (input.length() == 2 && input.charAt(0) >= 'a' && input.charAt(0) <= 'h') {
// reverse
input = "" + input.charAt(1) + input.charAt(0);
}
// recheck
if (checkCoordinateValidity(input)) {
System.out.println("! Non-compliant input. Do you mean \"" + input + "\"? (y/n)");
if (!System.console().readLine().toLowerCase().startsWith("y")) {
System.out.println("* Please try again.");
return null;
}
} else {
System.out.println("* Invalid input. Please try again.");
return null;
}
}
return input;
}
}
package chess;
//This class is partially implemented
public class Board {
private static Square[][] board = new Square[8][8];
// This method should not be edited
public static void initialiseBoard() {
for (int i = 0; i < board[0].length; i++) {
for (int j = 0; j < board[1].length; j++)
board[i][j] = new Square();
}
}
// This method requires your input
public static void initialisePieces() {
// Black pieces
setPiece(0, 0, new Rook(PieceColour.BLACK));
setPiece(0, 1, new Knight(PieceColour.BLACK));
setPiece(0, 2, new Bishop(PieceColour.BLACK));
setPiece(0, 3, new Queen(PieceColour.BLACK));
setPiece(0, 4, new King(PieceColour.BLACK));
setPiece(0, 5, new Bishop(PieceColour.BLACK));
setPiece(0, 6, new Knight(PieceColour.BLACK));
setPiece(0, 7, new Rook(PieceColour.BLACK));
setPiece(1, 0, new Pawn(PieceColour.BLACK));
setPiece(1, 1, new Pawn(PieceColour.BLACK));
setPiece(1, 2, new Pawn(PieceColour.BLACK));
setPiece(1, 3, new Pawn(PieceColour.BLACK));
setPiece(1, 4, new Pawn(PieceColour.BLACK));
setPiece(1, 5, new Pawn(PieceColour.BLACK));
setPiece(1, 6, new Pawn(PieceColour.BLACK));
setPiece(1, 7, new Pawn(PieceColour.BLACK));
// White pieces
setPiece(6, 0, new Pawn(PieceColour.WHITE));
setPiece(6, 1, new Pawn(PieceColour.WHITE));
setPiece(6, 2, new Pawn(PieceColour.WHITE));
setPiece(6, 3, new Pawn(PieceColour.WHITE));
setPiece(6, 4, new Pawn(PieceColour.WHITE));
setPiece(6, 5, new Pawn(PieceColour.WHITE));
setPiece(6, 6, new Pawn(PieceColour.WHITE));
setPiece(6, 7, new Pawn(PieceColour.WHITE));
setPiece(7, 0, new Rook(PieceColour.WHITE));
setPiece(7, 1, new Knight(PieceColour.WHITE));
setPiece(7, 2, new Bishop(PieceColour.WHITE));
setPiece(7, 3, new Queen(PieceColour.WHITE));
setPiece(7, 4, new King(PieceColour.WHITE));
setPiece(7, 5, new Bishop(PieceColour.WHITE));
setPiece(7, 6, new Knight(PieceColour.WHITE));
setPiece(7, 7, new Rook(PieceColour.WHITE));
}
// This method does not require your input
public static void printBoard() {
System.out.print("\n a b c d e f g h \n");
System.out.print(" -----------------\n");
for (int i = 0; i < board[0].length; i++) {
int row = i + 1;
for (int j = 0; j < board[1].length; j++) {
if ((j == 0) && Board.hasPiece(i, j))
System.out.print(row + " " + Board.getPiece(i, j).getSymbol());
else if ((j == 0) && !Board.hasPiece(i, j))
System.out.print(row + " ");
else if (Board.hasPiece(i, j))
System.out.print("|" + Board.getPiece(i, j).getSymbol());
else
System.out.print("| ");
}
System.out.print(" " + row + "\n");
}
System.out.print(" -----------------");
System.out.print("\n a b c d e f g h \n");
}
// This method requires your input
public static boolean movePiece(int i0, int j0, int i1, int j1, Piece p) {
boolean isKingCaptured = board[i1][j1].getPiece() instanceof King;
board[i0][j0].removePiece();
board[i1][j1].setPiece(p);
return isKingCaptured;
}
// This method requires your input
public static void setPiece(int iIn, int jIn, Piece p) {
board[iIn][jIn].setPiece(p);
}
// This method requires your input
public static Piece getPiece(int iIn, int jIn) {
return board[iIn][jIn].getPiece();
}
// This method requires your input
public static boolean hasPiece(int i, int j) {
return board[i][j].hasPiece();
}
}
package chess;
//This class requires your input
public class Square {
private boolean hasPiece = false;
private Piece p = null;
public Piece getPiece() {
return p;
}
public void setPiece(Piece piece) {
p = piece;
hasPiece = (p != null);
}
public void removePiece() {
p = null;
hasPiece = false;
}
public boolean hasPiece() {
return hasPiece;
}
}
在棋盘的每一个方格上,允许不包含或包含一种特定类型的棋子。棋子与方格之间形成0对1或1对1的聚合关系。
国际象棋中有多种棋子,每一种棋子除了符号、颜色、走法规则可能不同之外,都具有相同或类似的行为。因此,应该为所有类型的棋子设计一个共有的抽象类Piece,表示棋子拥有的共有属性和方法。在Piece所包含的方法中,有setters和getters供子类访问来设置和获取具体的符号和颜色,还有isLegitMove()方法判断该棋子的移动是否合法。由于每一种棋子的走法规则不同,该方法应设置为抽象方法要求子类来实现。
*注:本程序中只考虑国际象棋的最基本走子规则,不涉及吃过路兵(En passant),兵的升变(Promotion),王车易位(Castling)等。
棋子的颜色由枚举类型PieceColour表示,包含黑和白两个值。
棋子子类共六种,分别为兵Pawn,车Rook,象Bishop,后Queen,王King,马Knight,子类均继承自抽象类Piece。每一种子类都应根据规则对应重写各自的isLegitMove()方法。此外,还应包含构造函数,在被初始化时设置自身的颜色和相应符号。
至此,对UML对象的梳理全部结束。下面根据棋子类的设计完成相应的Java代码。
package chess;
//This class requires your input
public abstract class Piece {
private String symbol;
protected PieceColour colour;
public String getSymbol() {
return symbol;
}
public void setSymbol(String sym) {
symbol = sym;
}
public PieceColour getColour() {
return colour;
}
public abstract boolean isLegitMove(int i0, int j0, int i1, int j1);
}
package chess;
public enum PieceColour {
WHITE, BLACK;
}
package chess;
public class Pawn extends Piece {
public Pawn(PieceColour c) {
setSymbol(c == PieceColour.WHITE ? "\u2659" : "\u265f");
colour = c;
}
@Override
public boolean isLegitMove(int i0, int j0, int i1, int j1) {
// A pawn moves straight forward one square, if that square is vacant. If it has not yet moved, a pawn also has the option of moving two squares straight forward, provided both squares are vacant. Pawns cannot move backwards.
// Pawns are the only pieces that capture differently from how they move. A pawn can capture an enemy piece on either of the two squares diagonally in front of the pawn (but cannot move to those squares if they are vacant).
// use +-1 to represent two colors (this can act as the moving direction)
int c = Board.getPiece(i0, j0).getColour() == PieceColour.BLACK ? 1 : -1;
// move two squares straight forward (the i0 should be 1 or 6 according to BLACK/WHITE)
if (i0 == (c + 7) % 7 && i1 == i0 + c * 2 && j1 == j0) {
// both squares on the path should be vacant
return !Board.hasPiece(i0 + c, j0) && !Board.hasPiece(i1, j1);
}
else if (i1 == i0 + c && j1 == j0) {
// move straight forward one square, the destination should be vacant
return !Board.hasPiece(i1, j1);
}
else if (i1 == i0 + c && Math.abs(j1 - j0) == 1) {
// capture enemy piece (different color) diagonally in front of the pawn
return Board.hasPiece(i1, j1) && Board.getPiece(i0, j0).getColour() != Board.getPiece(i1, j1).getColour();
}
else
return false;
}
}
package chess;
public class Rook extends Piece {
public Rook(PieceColour c) {
setSymbol(c == PieceColour.WHITE ? "\u2656" : "\u265c");
colour = c;
}
@Override
public boolean isLegitMove(int i0, int j0, int i1, int j1) {
// A rook moves any number of vacant squares horizontally or vertically. (as long as no other pieces block in the middle)
if (i0 == i1) {
// move horizontally
for (int n = Math.min(j0, j1) + 1; n < Math.max(j0, j1); n++) {
if (Board.hasPiece(i1, n))
return false;
}
}
else if (j0 == j1) {
// move vertically
for (int n = Math.min(i0, i1) + 1; n < Math.max(i0, i1); n++) {
if (Board.hasPiece(n, j1))
return false;
}
}
else
return false;
// check whether the destination has pieces with same color
return !(Board.hasPiece(i1, j1) && Board.getPiece(i0, j0).getColour() == Board.getPiece(i1, j1).getColour());
}
}
package chess;
public class Bishop extends Piece {
public Bishop(PieceColour c) {
setSymbol(c == PieceColour.WHITE ? "\u2657" : "\u265d");
colour = c;
}
@Override
public boolean isLegitMove(int i0, int j0, int i1, int j1) {
// A bishop moves any number of vacant squares diagonally.
// check whether the movement is diagonal
if (Math.abs(i1 - i0) == Math.abs(j1 - j0)) {
// check whether other pieces block the way (two directions combined in one)
for (int n = 1; n < Math.abs(i1 - i0); n++) {
if (Board.hasPiece((i1 - i0 == j1 - j0) ? Math.min(i0, i1) + n : Math.max(i0, i1) - n, Math.min(j0, j1) + n))
return false;
}
}
else
return false;
// check whether the destination has pieces with same color
return !(Board.hasPiece(i1, j1) && Board.getPiece(i0, j0).getColour() == Board.getPiece(i1, j1).getColour());
}
}
package chess;
public class Queen extends Piece {
public Queen(PieceColour c) {
setSymbol(c == PieceColour.WHITE ? "\u2655" : "\u265b");
colour = c;
}
@Override
public boolean isLegitMove(int i0, int j0, int i1, int j1) {
// The queen moves any number of vacant squares horizontally, vertically, or diagonally. (as long as no other pieces block in the middle)
if (i0 == i1) {
// move horizontally
for (int n = Math.min(j0, j1) + 1; n < Math.max(j0, j1); n++) {
if (Board.hasPiece(i1, n))
return false;
}
}
else if (j0 == j1) {
// move vertically
for (int n = Math.min(i0, i1) + 1; n < Math.max(i0, i1); n++) {
if (Board.hasPiece(n, j1))
return false;
}
}
else if (Math.abs(i1 - i0) == Math.abs(j1 - j0)) {
// move diagonally (two directions combined in one)
for (int n = 1; n < Math.abs(i1 - i0); n++) {
if (Board.hasPiece((i1 - i0 == j1 - j0) ? Math.min(i0, i1) + n : Math.max(i0, i1) - n, Math.min(j0, j1) + n))
return false;
}
}
else
return false;
// check whether the destination has pieces with same color
return !(Board.hasPiece(i1, j1) && Board.getPiece(i0, j0).getColour() == Board.getPiece(i1, j1).getColour());
}
}
package chess;
public class King extends Piece {
public King(PieceColour c) {
setSymbol(c == PieceColour.WHITE ? "\u2654" : "\u265a");
colour = c;
}
@Override
public boolean isLegitMove(int i0, int j0, int i1, int j1) {
// The king moves exactly one square horizontally, vertically, or diagonally.
// only allowed to move within 3x3 grids and no pieces with same color on the destination
return Math.abs((i1 - i0) * (j1 - j0)) <= 1 && !(Board.hasPiece(i1, j1) && Board.getPiece(i0, j0).getColour() == Board.getPiece(i1, j1).getColour());
}
}
package chess;
public class Knight extends Piece {
public Knight(PieceColour c) {
setSymbol(c == PieceColour.WHITE ? "\u2658" : "\u265e");
colour = c;
}
@Override
public boolean isLegitMove(int i0, int j0, int i1, int j1) {
// A knight moves in an "L" pattern and is not blocked by other pieces.
// an "L" pattern means Δi * Δj == 2 and the destination cannot have same color pieces
return Math.abs((i1 - i0) * (j1 - j0)) == 2 && !(Board.hasPiece(i1, j1) && Board.getPiece(i0, j0).getColour() == Board.getPiece(i1, j1).getColour());
}
}
代码写完,来看最终的运行示例:
