技术人生,  游戏编程,  编程基础

Java初尝试:国际象棋

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

国际象棋UML图

统一建模语言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,这些内部属性由公共可见的几个settergetter方法来被安全的读写。

在具体考虑具体的棋子之前,我们可以把已经分析出的整体游戏结构实现为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所包含的方法中,有settersgetters供子类访问来设置和获取具体的符号和颜色,还有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());
    }
}

代码写完,来看最终的运行示例:

运行示例

A WindRunner. VoyagingOne

留言

您的电子邮箱地址不会被公开。