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()); } }
代码写完,来看最终的运行示例: