利用java開發簡易版掃雷遊戲

1.簡介

學瞭幾周的Java,閑來無事,寫個乞丐版的掃雷,加強一下Java基礎知識。

2.編寫過程

編寫這個遊戲,一共經歷瞭三個階段,編寫瞭三個版本的遊戲代碼。

第一版:完成瞭掃雷遊戲的基本雛形,實現瞭遊戲的基本功能,遊戲運行在cmd黑窗口中,以字符繪制遊戲界面,無圖形化窗口,通過控制臺輸入字符完成遊戲控制。代碼放置在一個java文件中,代碼的可讀性以及可擴展性都比較差。

第二版:在第一版實現基本功能的基礎之上,對遊戲代碼進行重構,根據各部分的功能創建多個類,增加代碼註釋,提高代碼的可讀性以及可擴展性。

第三版:在第二版重構代碼的基礎之上給遊戲增加瞭圖形化界面,將用戶從控制臺輸入命令控制遊戲變為通過鼠標左右鍵點擊操作控制遊戲。

3.遊戲運行邏輯

遊戲運行邏輯圖(第一版代碼):

流程圖1

遊戲運行邏輯圖(第二版代碼):

流程圖2

以上兩個遊戲流程圖的運行是建立在從控制臺讀取數據的基礎之上的,兩者的執行邏輯大體相同,其本質區別在於修改遊戲數據的時機不相同。前者是在通關判斷之前修改數據,後者實在通關判斷之後。兩者在運行期間並沒有什麼區別,但是當玩傢完成掃雷之後最後的畫面打印就會出現問題,即遊戲畫面中最後一個進行操作的坐標點的字符的顯示狀態,在發生改變之前就會終止程序。通過對修改遊戲數據以及通關判斷這兩個操作的執行順序進行調整,即可修正這一顯示錯誤。

遊戲運行邏輯圖(第三版代碼):

流程圖3

這個運行流程圖是基於第三版加瞭圖形化界面之後的遊戲代碼,遊戲控制流程與控制臺輸入控制的流程基本相同,隻是將從控制臺讀取用戶輸入變成瞭監聽用戶的鼠標左鍵與右鍵的點擊事件。並且,從控制臺讀取數據時要保證遊戲結束前一直進行讀取,因此需要設置while(true)循環以進行實現,遊戲結束使用break跳出循環。而在圖形化界面中,使用的是事件監聽器,事件監聽器在遊戲結束前持續監聽用戶的鼠標點擊事件,在遊戲結束的彈窗彈出的同時移除監聽器,結束鼠標對遊戲的控制。


4.遊戲相關數據存儲與讀取

以10x10x3的三維數組存儲每個坐標點的信息(包括行號、列號、是否是地雷、當前顯示的符號(未操作?、插旗#、地雷*)、周圍地雷數)。

一維數組的下標表示行號,二維數組的下標表示列號,三維數組中存的第一個數據表示的是該位置是否是地雷(0-不是,1-是),第二個數據表示該位置當前顯示的符號(0-?,1-#,2-*,3-顯示地雷數),第三個數據表示該位置周圍的地雷數。例如:

array[0][9][0]=1;表示將第0行第9列設置為地雷
array[0][9][1]=1;表示將第0行第9列的顯示字符設置為'#'
array[0][9][2]=0;表示將第0行第9列位置周圍的地雷數設置為0

三維數組

5.遊戲代碼

5.1 第一版

第一版的遊戲代碼寫在一個類中,主要的作用是實現基本的遊戲功能。代碼如下:

import java.util.Random;
import java.util.Scanner;
/**
 * 掃雷遊戲
 * @author zjl
 *
 */
public class MineSweeper {
	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		//初始化遊戲數據
		int[][][] gameData = init();
		while (true) {
			//打印遊戲信息
			showInfo();
			//打印遊戲框
			showWin(gameData);
			//踩中地雷結束遊戲
			//由於踩中地雷會把所有標記變成'*',所以隻需要判斷0行0列的顯示標記是不是'*'就行瞭
			if (gameData[0][0][1] == 2) {
				System.out.println("踩中地雷,遊戲結束!");
				break;
			}
			//通關結束遊戲
			if (missionAccomplished(gameData)) {
				System.out.println("恭喜通關!");
				break;
			}		
			//讀取控制臺數據並對遊戲數據數組進行修改
			gameData = readAndChangeData(input,gameData);
		}
	}
	/**
	 * 打印提示信息
	 */
	private static void showInfo() {
		printBlank(25);
		System.out.println("*******************************************************\n"
						 + "\t\t       遊戲信息\n"
						 + "遊戲名稱:掃雷\n"
						 + "遊戲版本:1.0\n"
						 + "遊戲操作:1.輸入行號及列號來選中要翻開的\'?\'進行操作,可\n"
						 + "\t  以選擇插旗(#)或者直接翻開.\n"
						 + "\t 2.如果翻開'*'則表示地雷,則遊戲結束;如果翻開\n"
						 + "\t  的是數字,則表示該格周圍的地雷數.\n"
						 + "\t 3.標記出全部地雷,並且沒有\'?\',則闖關成功,遊戲\n"
						 + "\t  結束.\n"
						 + "*******************************************************\n\n");
	}
	/**
	 * 打印遊戲框
	 */
	private static void showWin(int[][][] gameData) {
		System.out.println("    0 1 2 3 4 5 6 7 8 9\n"
						 + "  ***********************");
		//遍歷遊戲框中的每個坐標,讀取並打印顯示符號
		for (int i = 0; i < 10; i++) {
			System.out.print(i + " * ");
			for (int j = 0; j < 10; j++) {
				//讀取展示的符號
				char sign;
				switch (gameData[i][j][1]) {
				case 1:
					sign = '#';
					break;
				case 2:
					sign = '*';
					break;
				case 3:
					sign = (char)(gameData[i][j][2] + 48);
					break;
				default:
					sign = '?';
					break;
				}	
				//打印符號
				System.out.print(sign + " ");
			}	
			System.out.println("*");
		}	
		System.out.println("  ***********************");
	}
	/**
	 * 打印空白行
	 */
	private static void printBlank(int blankNum) {
		for (int i = 0; i < blankNum; i++) {
			System.out.println("");
		}
	}
	/**
	 * 隨機生成地雷坐標
	 */
	private static int[][] createMineCoord() {
		//定義二維數組
		int[][] mineCoordArray = new int[20][2];
		Random random = new Random();
		//將生成的隨機坐標存入數組中
		for (int i = 0; i < 20; i++) {
			for (int j = 0; j < 2; j++) {
				//生成0~9范圍內的隨機數
				int randomNumber = random.nextInt(10);
				mineCoordArray[i][j] = randomNumber;
			}
		}	
		return mineCoordArray;
	}
	/**
	 * 初始化遊戲數據
	 */
	private static int[][][] init(){
		//創建大小為10*10*3的三維數組(默認初始值為0)
		int[][][] gameData = new int[10][10][3];
		//生成隨機的地雷坐標,並將其存入遊戲數據數組中
		int[][] mineCoordArray = createMineCoord();
		for (int[] mineCoord : mineCoordArray) {
			int row = mineCoord[0];
			int col = mineCoord[1];
			gameData[row][col][0] = 1;
		}
		//計算每格周圍地雷數並將其存入遊戲數據數組中
		//循環遍歷每個坐標
		for (int i = 0; i < 10; i++) {
			for (int j = 0; j < 10; j++) {
				//遍歷當前坐標周圍的8個坐標
				for (int aroundRow = i-1; aroundRow <= i+1; aroundRow++) {
					//行號超范圍則跳過
					if (aroundRow < 0 || aroundRow > 9) {
						continue;
					}
					for (int aroundCol = j-1; aroundCol <= j+1; aroundCol++) {
						//列號超范圍則跳過
						if (aroundCol < 0 || aroundCol > 9) {
							continue;
						}
						//排除本身坐標點
						if ((gameData[aroundRow][aroundCol][0] == 1) && (!(aroundRow == i && aroundCol == j))) {
							gameData[i][j][2] += 1;
						}
					}
				}
			}
		}	
		return gameData;
	}
	/**
	 * 從控制臺讀取數據,並對遊戲的數據數組進行修改
	 * @param input
	 */
	private static int[][][] readAndChangeData(Scanner input,int[][][] gameData) {
		//定義在循環外部,以方便後續使用
		int row;
		int col;
		printBlank(12);
		//讀取輸入
		//設置循環來讀取行號,當輸入的行號不在范圍內時,會一直提示玩傢
		while (true) {
			System.out.print("請輸入行號:");
			row = input.nextInt();
			if (row >= 0 && row <= 9) {
				break;
			} else {
				System.out.println("輸入的行號不符合規范!");
			}
		}
		//設置循環來讀取列號,當輸入的行號不在范圍內時,會一直提示玩傢
		while(true) {
			System.out.print("請輸入列號:");
			col = input.nextInt();
			if (col >= 0 && col <= 9) {
				break;
			} else {
				System.out.println("輸入的列號不符合規范!");
			}
		}
		//設置循環,防止玩傢輸入不能識別的字符
		while (true) {
			System.out.print("標記(B)還是直接翻開(F):");
			String sign = input.next();
			//如果翻開的是炸彈,直接把所有標記變成'*',並返回結束遊戲
			if (sign.equalsIgnoreCase("f")) {
				if (gameData[row][col][0] == 1) {
					for (int i = 0; i < 10; i++) {
						for (int j = 0; j < 10; j++) {
							gameData[i][j][1] =2;
						}
					}
					break;
				}
			}
			//修改數據
			if (gameData[row][col][1] != 3) {//gameData[row][col][1] == 3 表示已被翻開,翻開的坐標點不能再被操作
				if (sign.equalsIgnoreCase("b")) {
					gameData[row][col][1] = 1;
				} else if (sign.equalsIgnoreCase("f")) {
					//如果翻開的不是炸彈,則顯示其周圍地雷數
					if (gameData[row][col][0] != 1) {
						gameData[row][col][1] = 3;				
					}
				} else {
					System.out.println("輸入不符合要求,請重新輸入!");
					continue;
				}
			}
			break;
		}	
		return gameData;
	}
	/**
	 * 通關判斷
	 * @return
	 */
	private static boolean missionAccomplished(int[][][] gameDate) {	
		//坐標點總數
		int totalSite = 10 * 10;
		//統計地雷數與非地雷數
		int mineSigned = 0;
		int noMineOpen = 0;
		//遍歷遊戲數據數組
		for (int i = 0; i < 10; i++) {
			for (int j = 0; j < 10; j++) {
				//通關條件
				//1、翻開非地雷的位置
				if (gameDate[i][j][0] == 0 && gameDate[i][j][1] == 3) {
					noMineOpen++;
				}
				//2、地雷位置標記
				if (gameDate[i][j][0] == 1 && gameDate[i][j][1] == 1) {
					mineSigned++;
				}
			}
		}
		if (totalSite == (noMineOpen + mineSigned)) {
			return true;
		}
		return false;
	}
}

5.2 第二版

這一版的代碼實在第一版的基礎上對代碼進行重構,根據功能對代碼進行分類,將其放入不同的類中,增加代碼的可讀性與可維護性、可擴展性。代碼一共分為五個類:主程序類、設置類、地雷類、控制類、顯示類。各部分代碼各司其職,共同作用,共同完成遊戲運行。

主程序類,遊戲程序入口:

import java.util.Scanner;
/**
 * 掃雷遊戲
 * @author zjl
 *
 */
public class MineSweeper {
	/**
	 * 遊戲運行主程序
	 * @param args
	 */
	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		//初始化遊戲數據
		int[][][] gameData = GameControl.init();
		while (true) {
			//打印遊戲信息
			Show.gameInfo();
			//打印遊戲框
			Show.gameBoard(gameData);
			//踩中地雷結束遊戲
			//由於踩中地雷會把所有標記變成'*',所以隻需要判斷0行0列的顯示標記是不是'*'就行瞭
			if (gameData[0][0][Settings.SIGN_DATA] == Settings.MINE_SIGN_DATA) {
				System.out.println("踩中地雷,遊戲結束!");
				break;
			}
			//通關結束遊戲
			if (GameControl.missionAccomplished(gameData)) {
				System.out.println("恭喜通關!");
				break;
			}
			//讀取控制臺數據並對遊戲數據數組進行修改
			GameControl.readAndChangeData(input,gameData);		
		}
	}
}

設置類,遊戲相關設置數據:

/**
 * 定義遊戲初始數據的類
 * @author zjl
 *
 */
public class Settings {
	//定義遊戲界面參數
	/**
	 * 遊戲界面的行數
	 */
	public static final int ROW_SIZE = 10;
	/**
	 * 遊戲界面的列數
	 */
	public static final int COL_SIZE = 10;
	/**
	 * 兩個遊戲界面之間的默認空白行數
	 */
	public static final int DEFAULT_BLANK = 20;
	/**
	 * 地雷數
	 */
	public static final int MINE_NUM = 20;
	/**
	 * 確定地雷位置所需要的坐標數,由於是在平面內,所以隻需要設置橫縱坐標,值為2
	 */
	public static final int MINE_SITE_NUM = 2;
	/**
	 * 地雷行坐標在地雷數組中的下標
	 */
	public static final int MINE_ROW_IN_ARRAY = 0;
	/**
	 * 地雷列坐標在地雷數組中的下標
	 */
	public static final int MINE_COL_IN_ARRAY = 1;
	/**
	 * 每個坐標點中存儲數據的數組的大小
	 */
	public static final int DATA_SIZE = 3;
	//定義每個坐標點中存儲數據的數組中數值的含義
	/**
	 * 數組中存放地雷信息的位置
	 */
	public static final int MINE_DATA = 0;
	/**
	 * 表示不是地雷,為默認值
	 */
	public static final int IS_NOT_MINE = 0;
	/**
	 * 表示是地雷
	 */
	public static final int IS_MINE = 1;
	/**
	 * 數組中存放符號信息的位置
	 */
	public static final int SIGN_DATA = 1;
	/**
	 * 表示初始符號,即'?'
	 */
	public static final int INIT_SIGN_DATA = 0;
	/**
	 * 表示插旗符號,即'#'
	 */
	public static final int FLAG_SIGN_DATA = 1;
	/**
	 * 表示地雷符號,即'*'
	 */
	public static final int MINE_SIGN_DATA = 2;
	/**
	 * 表示當前位置已翻開,即應該顯示當前位置的地雷數
	 */
	public static final int MINE_NUM_SIGN_DATA = 3;
	/**
	 * 數組中存放坐標點周圍地雷數的位置
	 */
	public static final int AROUND_MINE_DATA = 2;
	//遊戲符號
	/**
	 * 初始符號'?'
	 */
	public static final char INIT_SIGN = '?';
	/**
	 * 插旗符號'#'
	 */
	public static final char FLAG_SIGN = '#';
	/**
	 * 地雷符號'*'
	 */
	public static final char MINE_SIGN = '*';
	/**
	 * 在ASCII碼表中整數48~57代表字符0~9,設置一個增量值,將數字轉換為字符
	 */
	public static final int ASCII_ADD = 48;
	//定義玩傢在控制臺輸入的操縱符
	/**
	 * 表示翻開操縱的符號
	 */
	public static final String OPEN_OPERATION = "F";
	/**
	 * 表示插旗操作的符號
	 */
	public static final String FLAG_OPERATION = "B";
	/**
	 * 遊戲信息
	 */
	public static final String INFORMATION = "**************************************************\n"
											 + "\t\t       遊戲信息\n"
											 + "遊戲名稱:掃雷\n"
											 + "遊戲版本:2.0\n"
											 + "遊戲操作:1.輸入行號及列號來選中要翻開的\'?\'進行操作,可\n"
											 + "\t  以選擇插旗(#)或者直接翻開.\n"
											 + "\t 2.如果翻開'*'則表示地雷,則遊戲結束;如果翻開\n"
											 + "\t  的是數字,則表示該格周圍的地雷數.\n"
											 + "\t 3.標記出全部地雷,並且沒有\'?\',則闖關成功,遊戲\n"
											 + "\t  結束.\n"
											 + "**************************************************\n\n";	
}

地雷類,生成隨機的地雷坐標數據:

import java.util.Random;
/**
 * 有關地雷的類
 * @author zjl
 *
 */
public class Mine {
	/**
	 * 隨機生成地雷坐標
	 */
	public static int[][] createMineCoord() {
		//定義二維數組
		int[][] mineCoordArray = new int[Settings.MINE_NUM][Settings.MINE_SITE_NUM];
		Random random = new Random();
		//將生成的隨機坐標存入數組中
		for (int i = 0; i < Settings.MINE_NUM; i++) {
			for (int j = 0; j < Settings.MINE_SITE_NUM; j++) {
				//生成行坐標隨機數,並將其放入數組
				if (j == Settings.MINE_ROW_IN_ARRAY) {
					mineCoordArray[i][j] = random.nextInt(Settings.ROW_SIZE);
				}
				//生成列坐標隨機數,並將其放入數組
				if (j == Settings.MINE_COL_IN_ARRAY) {
					mineCoordArray[i][j] = random.nextInt(Settings.COL_SIZE);
				}
			}
		}
		return mineCoordArray;
	}
}

控制類,控制遊戲進程,以及遊戲數據:

import java.util.Scanner;

/**
 * 關於遊戲相關控制的類
 * @author zjl
 *
 */
public class GameControl {
	
	/**
	 * 初始化遊戲數據
	 */
	public static int[][][] init(){
		//創建存儲遊戲相關數據的三維數組(默認初始值為0)
		int[][][] gameData = new int[Settings.ROW_SIZE][Settings.COL_SIZE][Settings.DATA_SIZE];
		
		//生成隨機的地雷坐標,並將其存入遊戲數據數組中
		int[][] mineCoordArray = Mine.createMineCoord();
		for (int[] mineCoord : mineCoordArray) {
			int row = mineCoord[Settings.MINE_ROW_IN_ARRAY];
			int col = mineCoord[Settings.MINE_COL_IN_ARRAY];
			gameData[row][col][Settings.MINE_DATA] = Settings.IS_MINE;
		}
		//計算每格周圍地雷數並將其存入遊戲數據數組中
		//循環遍歷每個坐標
		for (int i = 0; i < Settings.ROW_SIZE; i++) {
			for (int j = 0; j < Settings.COL_SIZE; j++) {
				//遍歷當前坐標周圍的8個坐標
				for (int aroundRow = i-1; aroundRow <= i+1; aroundRow++) {
					//行號超范圍則跳過
					if (aroundRow < 0 || aroundRow > Settings.ROW_SIZE-1) {
						continue;
					}
					for (int aroundCol = j-1; aroundCol <= j+1; aroundCol++) {
						//列號超范圍則跳過
						if (aroundCol < 0 || aroundCol > Settings.COL_SIZE-1) {
							continue;
						}
						//排除本身坐標點
						if ((gameData[aroundRow][aroundCol][Settings.MINE_DATA] == Settings.IS_MINE) && (!(aroundRow == i && aroundCol == j))) {
							gameData[i][j][Settings.AROUND_MINE_DATA] += 1;
						}
					}
				}
			}
		}
		return gameData;
	}
	/**
	 * 從控制臺讀取數據,並對遊戲的數據數組進行修改
	 * @param input
	 */
	public static void readAndChangeData(Scanner input,int[][][] gameData) {
		//定義在循環外部,以方便後續使用
		int row;
		int col;
		//讀取輸入
		//設置循環來讀取行號,當輸入的行號不在范圍內時,會一直提示玩傢
		while (true) {
			System.out.print("請輸入行號:");
			row = input.nextInt();
			if (row >= 0 && row <= Settings.ROW_SIZE-1) {
				break;
			} else {
				System.out.println("輸入的行號不符合規范!");
			}
		}
		//設置循環來讀取列號,當輸入的行號不在范圍內時,會一直提示玩傢
		while(true) {
			System.out.print("請輸入列號:");
			col = input.nextInt();
			if (col >= 0 && col <= Settings.COL_SIZE-1) {
				break;
			} else {
				System.out.println("輸入的列號不符合規范!");
			}
		}
		//設置循環,防止玩傢輸入不能識別的字符
		while (true) {
			System.out.print("標記(B)還是直接翻開(F):");
			String sign = input.next();
			
			//如果翻開的是炸彈,直接把所有標記變成'*',並返回結束遊戲
			if (sign.equalsIgnoreCase(Settings.OPEN_OPERATION)) {
				if (gameData[row][col][Settings.MINE_DATA] == Settings.IS_MINE) {
					for (int i = 0; i < Settings.ROW_SIZE; i++) {
						for (int j = 0; j < Settings.COL_SIZE; j++) {
							gameData[i][j][Settings.SIGN_DATA] =Settings.MINE_SIGN_DATA;
						}
					}
					break;
				}
			}
			//修改數據
			if (gameData[row][col][Settings.SIGN_DATA] != Settings.MINE_NUM_SIGN_DATA) {//相等表示已被翻開,翻開的坐標點不能再被操作
				if (sign.equalsIgnoreCase(Settings.FLAG_OPERATION)) {
					gameData[row][col][Settings.SIGN_DATA] = Settings.FLAG_SIGN_DATA;
				} else if (sign.equalsIgnoreCase(Settings.OPEN_OPERATION)) {
					//如果翻開的不是炸彈,則顯示其周圍地雷數
					if (gameData[row][col][Settings.MINE_DATA] == Settings.IS_NOT_MINE) {
						gameData[row][col][Settings.SIGN_DATA] = Settings.MINE_NUM_SIGN_DATA;
						
					}
				} else {
					System.out.println("輸入不符合要求,請重新輸入!");
					continue;
				}
			}
			break;
		}
	}
	/**
	 * 通關判斷
	 * @return
	 */
	public static boolean missionAccomplished(int[][][] gameDate) {
		//坐標點總數
		int totalSite = Settings.ROW_SIZE * Settings.COL_SIZE;
		//統計地雷數與非地雷數
		int mineSigned = 0;
		int noMineOpen = 0;
		//遍歷遊戲數據數組
		for (int i = 0; i < Settings.ROW_SIZE; i++) {
			for (int j = 0; j < Settings.COL_SIZE; j++) {
				//通關條件
				//1、翻開非地雷的位置
				if (gameDate[i][j][Settings.MINE_DATA] == Settings.IS_NOT_MINE && gameDate[i][j][Settings.SIGN_DATA] == Settings.MINE_NUM_SIGN_DATA) {
					noMineOpen++;
				}
				//2、地雷位置標記
				if (gameDate[i][j][Settings.MINE_DATA] == Settings.IS_MINE && gameDate[i][j][Settings.SIGN_DATA] == Settings.FLAG_SIGN_DATA) {
					mineSigned++;
				}
			}
		}
		//當翻開的的坐標數加上標記的地雷數等於坐標點總數的時候,返回true表示可以結束遊戲
		if (totalSite == (noMineOpen + mineSigned)) {
			return true;
		}
		//條件不滿足,遊戲繼續
		return false;
	}
}

顯示類,對遊戲的相關畫面進行打印:

/**
 * 展示遊戲相關畫面的類
 * @author zjl
 *
 */
public class Show {
	/**
	 * 打印提示信息
	 */
	public static void gameInfo() {
		//打印空白行,作用是使展現在控制臺的圖形刷新
		printSign(Settings.DEFAULT_BLANK,"\n");
		System.out.println(Settings.INFORMATION);
	}
	/**
	 * 打印一行指定的圖形
	 */
	public static void printSign(int num,String sign) {
		for (int i = 0; i < num; i++) {
			System.out.print(sign);
		}
	}
	/**
	 * 打印遊戲框
	 */
	public static void gameBoard(int[][][] gameData) {
		//打印遊戲上邊框
		printSign(4, " ");
		for (int i = 0; i < Settings.COL_SIZE; i++) {
			printSign(1, i+" ");
		}
		printSign(1, "\n  *");
		printSign(Settings.COL_SIZE+1, "**");
		printSign(1, "\n");
		//遍歷遊戲框中的每個坐標,讀取並打印顯示符號
		for (int i = 0; i < Settings.ROW_SIZE; i++) {
			System.out.print(i + " * ");
			for (int j = 0; j < Settings.COL_SIZE; j++) {
				//讀取展示的符號
				char sign;
				switch (gameData[i][j][Settings.SIGN_DATA]) {
				case Settings.FLAG_SIGN_DATA:
					sign = Settings.FLAG_SIGN;
					break;
				case Settings.MINE_SIGN_DATA:
					sign = Settings.MINE_SIGN;
					break;
				case Settings.MINE_NUM_SIGN_DATA:
					//將數組中存的整型數值通過ASCII碼轉為字符型表示
					sign = (char)(gameData[i][j][Settings.AROUND_MINE_DATA] + Settings.ASCII_ADD);
					break;
				default:
					sign = Settings.INIT_SIGN;
					break;
				}
				//打印符號
				System.out.print(sign + " ");
			}
			System.out.println("*");
		}
		//打印遊戲下邊框
		printSign(2, " ");
		printSign(Settings.COL_SIZE+1, "**");
		printSign(1, "*\n");
	}
}

5.3 第三版

在第二版的基礎上,去除瞭顯示類,增加瞭圖片類、圖形界面類、事件監聽器類。

遊戲運行主程序類:

/**
 * 掃雷遊戲主程序類
 * @author zjl
 *
 */
public class MineSweeper {
	/**
	 * 遊戲運行主程序
	 * @param args
	 */
	public static void main(String[] args) {
		int[][][] gameData = new int[Settings.ROW_SIZE][Settings.COL_SIZE][Settings.DATA_SIZE];
		//創建遊戲控制類對象
		GameDataController controller = new GameDataController(gameData);
		//初始化遊戲數據
		controller.init();
		//繪制遊戲界面
		new Graphic(controller);
	}
}

設置類,提供遊戲運行相關這是數據:

/**
 * 定義遊戲初始數據的類
 * @author zjl
 *
 */
public class Settings {
	//定義遊戲界面參數
	/**
	 * 遊戲界面的行數
	 */
	public static final int ROW_SIZE = 10;
	/**
	 * 遊戲界面的列數
	 */
	public static final int COL_SIZE = 10;
	
	/**
	 * 地雷數
	 */
	public static final int MINE_NUM = 20;
	/**
	 * 確定地雷位置所需要的坐標數,由於是在平面內,所以隻需要設置橫縱坐標,值為2
	 */
	public static final int MINE_SITE_NUM = 2;
	/**
	 * 地雷行坐標在地雷數組中的下標
	 */
	public static final int MINE_ROW_IN_ARRAY = 0;
	/**
	 * 地雷列坐標在地雷數組中的下標
	 */
	public static final int MINE_COL_IN_ARRAY = 1;
	
	/**
	 * 每個坐標點中存儲數據的數組的大小
	 */
	public static final int DATA_SIZE = 3;
	//定義每個坐標點中存儲數據的數組中數值的含義
	/**
	 * 數組中存放地雷信息的位置
	 */
	public static final int MINE_DATA = 0;
	/**
	 * 表示不是地雷,為默認值
	 */
	public static final int IS_NOT_MINE = 0;
	/**
	 * 表示是地雷
	 */
	public static final int IS_MINE = 1;
	/**
	 * 數組中存放符號信息的位置
	 */
	public static final int SIGN_DATA = 1;
	/**
	 * 表示初始符號
	 */
	public static final int INIT_SIGN_DATA = 0;
	/**
	 * 表示插旗符號
	 */
	public static final int FLAG_SIGN_DATA = 1;
	/**
	 * 表示地雷符號
	 */
	public static final int MINE_SIGN_DATA = 2;
	/**
	 * 表示當前位置已翻開,即應該顯示當前位置的地雷數
	 */
	public static final int MINE_NUM_SIGN_DATA = 3;
	/**
	 * 數組中存放坐標點周圍地雷數的位置
	 */
	public static final int AROUND_MINE_DATA = 2;
	
	//定義遊戲框尺寸數據
	/**
	 * 圖片邊長
	 */
	public static final int IMAGE_SIZE = 60;
	/**
	 * 遊戲窗口在屏幕上的x位置
	 */
	public static final int FRAME_X = 400;
	/**
	 * 遊戲窗口在屏幕上的y位置
	 */
	public static final int FRAME_Y = 150;
	/**
	 * 遊戲窗口的寬度
	 */
	public static final int FRAME_WIDTH = IMAGE_SIZE * ROW_SIZE;
	/**
	 * 遊戲窗口的高度
	 */
	public static final int FRAME_HEIGHT =IMAGE_SIZE * COL_SIZE;
	/**
	 * 遊戲網格線的寬度
	 */
	public static final int BORDER_WIDTH = 1;
	/**
	 * 遊戲窗口標題欄高度
	 */
	public static final int TITLE_HEIGHT = 23;
	
	//彈窗數據
	/**
	 * 彈窗標題
	 */
	public static final String DIALOG_TITLE = "提示";
	/**
	 * 彈窗提示語,踩雷
	 */
	public static final String DIALOG_DEFEAT = "踩雷,遊戲結束!";
	/**
	 * 彈窗提示語,通關
	 */
	public static final String DIALOG_VECTORY = "恭喜通關!";
}

圖片類,提供遊戲相關圖片:

import javax.swing.ImageIcon;

/**
 * 這是一個用於提供遊戲所需圖片的類,在遊戲運行時,將相關的圖片加載到圖形界面上
 * @author zjl
 *
 */
public class Image{
	/**
	 * ImageIcon類型的常量,表示地雷圖片
	 */
	public static final ImageIcon IMAGE_MINE = new ImageIcon("img\\mine.png");
	/**
	 * ImageIcon類型的常量,表示旗幟圖片
	 */
	public static final ImageIcon IMAGE_FLAG = new ImageIcon("img\\flag.png");
	/**
	 * ImageIcon類型的常量,表示失敗時的表情圖片
	 */
	public static final ImageIcon IMAGE_DEFEAT = new ImageIcon("img\\defeat.png");
	/**
	 * ImageIcon類型的常量,表示通關時的表情圖片
	 */
	public static final ImageIcon IMAGE_VECTORY = new ImageIcon("img\\vectory.png");
	
	/**
	 * 這是一個用於返回數字圖片的靜態方法,
	 * 通過傳入的參數來獲取表示對應數字的圖片,
	 * 返回的圖片上的數字表示某位置周圍存在的地雷數
	 * @param mineNum-int類型,表示傳入地雷數量的參數
	 * @return image-ImageIcon類型的返回值,返回的圖片上的數字與參數mineNum對應
	 */
	public static ImageIcon getImageByNum(int mineNum) {
		ImageIcon image = new ImageIcon("img\\"+mineNum+".png");
		return image;
	}
}

地雷類,生成遊戲中的類的相關數據:

import java.util.Random;

/**
 * 這是一個用於生成隨機地雷坐標的類。
 * 由於生成的是偽隨機數,因此生成的地雷坐標可能重復,所以實際遊戲中的地雷數量並不固定。
 * @author zjl
 *
 */
public class Mine {
	
	/**
	 * 隨機生成坐標數據,為遊戲提供隨機的地雷坐標數據。
	 * @return mineCoordArray-int類型的二維數組,存儲的是生成的地雷的坐標數據
	 */
	public static int[][] createMineCoord() {
		//定義二維數組
		int[][] mineCoordArray = new int[Settings.MINE_NUM][Settings.MINE_SITE_NUM];
		Random random = new Random();
		//將生成的隨機坐標存入數組中
		for (int i = 0; i < Settings.MINE_NUM; i++) {
			for (int j = 0; j < Settings.MINE_SITE_NUM; j++) {
				//生成行坐標隨機數,並將其放入數組
				if (j == Settings.MINE_ROW_IN_ARRAY) {
					mineCoordArray[i][j] = random.nextInt(Settings.ROW_SIZE);
				}
				//生成列坐標隨機數,並將其放入數組
				if (j == Settings.MINE_COL_IN_ARRAY) {
					mineCoordArray[i][j] = random.nextInt(Settings.COL_SIZE);
				}
			}
		}
		
		return mineCoordArray;
	}
}

遊戲控制類,提供用於遊戲控制的相關方法:

import javax.swing.JLabel;

/**
 * 這是一個用於遊戲數據控制的類,
 * @author zjl
 *
 */
public class GameDataController {
	/**
	 * 私有的成員變量,用於存儲在構造方法中接收到的遊戲數據
	 */
	private int[][][] gameData;
	/**
	 * 這是本類的一個有參構造方法,通過傳入遊戲數據來構造一個遊戲數據控制器
	 * @param gameData-存儲遊戲相關數據的三維數組
	 */
	public GameDataController(int[][][] gameData) {
		this.gameData = gameData;
	}
	/**
	 * 初始化遊戲數據
	 */
	public void init(){
		//將地雷數據存入三維遊戲數組中
		int[][] mineCoordArray = Mine.createMineCoord();
		for (int[] mineCoord : mineCoordArray) {
			int row = mineCoord[Settings.MINE_ROW_IN_ARRAY];
			int col = mineCoord[Settings.MINE_COL_IN_ARRAY];
			gameData[row][col][Settings.MINE_DATA] = Settings.IS_MINE;
		}
		//計算每格周圍地雷數並將其存入遊戲數據數組中
		calcAroundNum();
	}
	/**
	 * 遊戲通關判斷
	 * @return 返回boolean類型的true或false,true表示遊戲通關
	 */
	public boolean missionAccomplished() {
		//坐標點總數
		int totalSite = Settings.ROW_SIZE * Settings.COL_SIZE;
		//統計地雷數與非地雷數
		int mineSigned = 0;
		int noMineOpen = 0;
		//遍歷遊戲數據數組
		for (int i = 0; i < Settings.ROW_SIZE; i++) {
			for (int j = 0; j < Settings.COL_SIZE; j++) {
				//通關條件
				//1、翻開非地雷的位置
				if (gameData[i][j][Settings.MINE_DATA] == Settings.IS_NOT_MINE && gameData[i][j][Settings.SIGN_DATA] == Settings.MINE_NUM_SIGN_DATA) {
					noMineOpen++;
				}
				//2、地雷位置標記
				if (gameData[i][j][Settings.MINE_DATA] == Settings.IS_MINE && gameData[i][j][Settings.SIGN_DATA] == Settings.FLAG_SIGN_DATA) {
					mineSigned++;
				}
			}
		}
		//當翻開的的坐標數加上標記的地雷數等於坐標點總數的時候,返回true表示可以結束遊戲
		if (totalSite == (noMineOpen + mineSigned)) {
			return true;
		}
		//條件不滿足,遊戲繼續
		return false;
	}
	/**
	 * 用於鼠標左擊時的遊戲控制操作,即翻開所點擊的位置
	 * @param x-表示點擊位置在遊戲框上的x軸坐標
	 * @param y-表示點擊位置在遊戲框上的y軸坐標
	 * @param labels-表示用於放置圖片的標簽的集合
	 */
	public void leftClick(int x, int y, JLabel[][] labels) {
		
		if (gameData[y][x][Settings.SIGN_DATA] == Settings.INIT_SIGN_DATA) {
			if (gameData[y][x][Settings.MINE_DATA] == Settings.IS_MINE) {
				//如果翻開的是地雷,顯示彈窗提示結束遊戲
				labels[y][x].setIcon(Image.IMAGE_MINE);
				Graphic.showDialog(false);
			}else {
				//如果當前位置未被翻開,則翻開當前位置,修改遊戲數據及顯示圖片
				gameData[y][x][Settings.SIGN_DATA] = Settings.MINE_NUM_SIGN_DATA;
				int aroundMineNum = gameData[y][x][Settings.AROUND_MINE_DATA];
				labels[y][x].setIcon(Image.getImageByNum(aroundMineNum));
			}
		}
	}
	/**
	 * 用於鼠標右擊時的遊戲控制操作,即插旗與取消插旗
	 * @param x-表示點擊位置在遊戲框上的x軸坐標
	 * @param y-表示點擊位置在遊戲框上的y軸坐標
	 * @param labels-表示用於放置圖片的標簽的集合
	 */
	public void rightClick(int x, int y, JLabel[][] labels) {
		
		if (gameData[y][x][Settings.SIGN_DATA] == Settings.INIT_SIGN_DATA) {
			//如果當前位置未被翻開,則修改相應數據,並將其顯示為插旗
			gameData[y][x][Settings.SIGN_DATA] = Settings.FLAG_SIGN_DATA;
			labels[y][x].setIcon(Image.IMAGE_FLAG);
		} else if (gameData[y][x][Settings.SIGN_DATA] == Settings.FLAG_SIGN_DATA) {
			//如果該位置已被插旗,則修改相應數據,並將其恢復初始狀態
			gameData[y][x][Settings.SIGN_DATA] = Settings.INIT_SIGN_DATA;
			labels[y][x].setIcon(null);
		}
	}
	/**
	 * 計算每個位置周圍的地雷數,並將算出的結果存入到三維遊戲數組中
	 */
	private void calcAroundNum() {
		
		for (int i = 0; i < Settings.ROW_SIZE; i++) {
			for (int j = 0; j < Settings.COL_SIZE; j++) {
				//遍歷當前坐標周圍的8個坐標
				for (int aroundRow = i-1; aroundRow <= i+1; aroundRow++) {
					//行號超范圍則跳過
					if (aroundRow < 0 || aroundRow > Settings.ROW_SIZE-1) {
						continue;
					}
					for (int aroundCol = j-1; aroundCol <= j+1; aroundCol++) {
						//列號超范圍則跳過
						if (aroundCol < 0 || aroundCol > Settings.COL_SIZE-1) {
							continue;
						}
						//排除本身坐標點
						if ((gameData[aroundRow][aroundCol][Settings.MINE_DATA] == Settings.IS_MINE) && (!(aroundRow == i && aroundCol == j))) {
							gameData[i][j][Settings.AROUND_MINE_DATA] += 1;
						}
					}
				}
			}
		}
	}
}

繪制圖形化界面類,生成顯示遊戲的圖形化界面:

import java.awt.Color;
import java.awt.GridLayout;

import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;

/**
 * 繪制掃雷的圖形界面的類,提供用於繪制界面以及用於獲取相關對象的方法
 * @author zjl
 *
 */
public class Graphic {
	/**
	 * 定義JFrame類型的的靜態屬性frame
	 */
	private static JFrame frame;
	/**
	 * 定義GameListener類型的遊戲事件監聽器gameListener
	 */
	private static GameListener gameListener;
	/**
	 * 定義用於存儲JLabel類型數據的二維數組labels
	 */
	private JLabel[][] labels = new JLabel[Settings.ROW_SIZE][Settings.COL_SIZE];
	/**
	 * 定義JLabel類型的屬性label
	 */
	private JLabel label;
	/**
	 * 定義遊戲控制器
	 */
	private GameDataController controller;
	/**
	 * 初始化遊戲圖形界面中窗口容器的相關設置
	 */
	static {
		frame = new JFrame("掃雷2.0");
		//將frame的佈局管理器設置為GridLayout
		frame.setLayout(new GridLayout(Settings.ROW_SIZE, Settings.COL_SIZE));
		//設置frame的位置、大小、可見性,設置窗體大小不可更改以及關閉按鈕的功能
		frame.setBounds(Settings.FRAME_X, Settings.FRAME_Y, Settings.FRAME_WIDTH, Settings.FRAME_HEIGHT);
		frame.setVisible(true);
		frame.setResizable(false);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	}
	/**
	 * 這是該類的一個有參構造方法,用於創建Graphic對象。
	 * 該構造方法調用本類中的draw()方法,繪制遊戲的圖形化界面
	 * @param controller-接收主函數中傳入的遊戲參數控制器對象,後續將其傳遞到事件監聽器中,使監聽器能夠修改遊戲數據
	 */
	public Graphic(GameDataController controller) {
		this.controller = controller;
		draw();
	}
	/**
	 * 繪制遊戲的圖形化界面,並在遊戲窗口上添加事件監聽器
	 */
	private void draw() {
		//通過循環創建label,並將其加入到frame中
		for (int i = 0; i < Settings.ROW_SIZE; i++) {
			for (int j = 0; j < Settings.COL_SIZE; j++) {
				frame.add(label = new JLabel());
				labels[i][j] = label;
				//設置label的邊框屬性
				label.setBorder(BorderFactory.createLineBorder(Color.BLACK, Settings.BORDER_WIDTH));
			}
		}
		//創建事件監聽器,監聽鼠標點擊在frame上的位置,並將監聽器添加到frame上
		gameListener = new GameListener(labels, controller);
		frame.addMouseListener(gameListener);
	}
	/**
	 * 繪制遊戲結束時的彈窗。
	 * 根據傳入的參數判斷遊戲是因為踩到地雷而結束還是因為通關而結束,從而繪制不同效果的彈窗
	 * @param result-boolean類型的參數,表示遊戲是因為通關結束還是因為踩雷結束
	 */
	public static void showDialog(boolean result) {
		int option;
		String message;
		ImageIcon image;
		//判斷遊戲的結束原因,並進行相應的賦值操作
		if (result) {
			message = Settings.DIALOG_VECTORY;
			image = Image.IMAGE_VECTORY;
		} else {
			message = Settings.DIALOG_DEFEAT;
			image = Image.IMAGE_DEFEAT;
		}
		//彈窗出現表示遊戲結束,此時應移除窗體上的事件監聽器
		frame.removeMouseListener(gameListener);
		//根據相關參數繪制彈窗
		option = JOptionPane.showConfirmDialog(null, message, Settings.DIALOG_TITLE, JOptionPane.CANCEL_OPTION,JOptionPane.INFORMATION_MESSAGE,image);
		/* 根據彈窗上的按鈕點擊結果判斷是否關閉遊戲退出程序。
		 * 隻有在點擊確定時才會結束程序,點擊取消並不會推出遊戲,
		 * 而是停留在遊戲結束時的畫面,但是不能進行遊戲操作,
		 * 點擊關閉窗口即可退出程序
		 */
		if (option != JOptionPane.CANCEL_OPTION) {
			System.exit(0);
		}
	}
}

事件監聽器類,用於提供監聽鼠標點擊事件的監聽器:

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.ImageIcon;
import javax.swing.JLabel;

/**
 * 定義遊戲相關的事件監聽器
 * @author zjl
 *
 */
public class GameListener extends MouseAdapter {
	/**
	 * 私有的成員變量,用以接收構造事件監聽器時傳入的JLabel數組
	 */
	private JLabel[][] labels;
	/**
	 * 私有的成員變量,用以接收構造事件監聽器時傳入的遊戲數據控制器對象
	 */
	private GameDataController controller;
	/**
	 * 這是本類一個有參構造方法,用於根據傳入的參數構造事件監聽器對象
	 * @param labels-這是一個JLabel數組,為後續改變相應的圖像顯示提供容納圖片的JLabel組件
	 * @param controller-遊戲數據控制器對象,用於改變相應遊戲數據
	 */
	public GameListener(JLabel[][] labels, GameDataController controller) {
		this.labels = labels;
		this.controller = controller;
	}
	/**
	 * 重寫MouseAdapter類中的mouseClicked方法,添加響應鼠標操作的邏輯代碼
	 */
	 @Override
	public void mouseClicked(MouseEvent e) {
		//對鼠標點擊點的坐標進行計算可得到label在數組中的下標
		int x = (e.getX()-Settings.BORDER_WIDTH)/Settings.IMAGE_SIZE;
		int y = (e.getY()-Settings.TITLE_HEIGHT)/Settings.IMAGE_SIZE;
		//區分鼠標左擊右擊事件
		if (e.getButton() == MouseEvent.BUTTON1) {//鼠標左擊,進行的操作為翻開當前位置
			controller.leftClick(x, y, labels);
		} else if (e.getButton() == MouseEvent.BUTTON3) {//鼠標右擊,進行的操作為插旗與取消插旗
			controller.rightClick(x, y, labels);
		}
		//通關判斷
		if (controller.missionAccomplished()) {
			//通關則顯示彈窗並移除監聽器
			Graphic.showDialog(true);
		}
	}
}

6.部分代碼思路

6.1 生成隨機的地雷坐標

創建Random類的對象,使用相關方法,生成地雷行坐標與列坐標的隨機數值,使用二維數組存儲坐標點數據。由於沒有做去重處理,因此有概率生成多個相同的坐標,所以地雷數最多為設置的生成數,最少為1(概率極低)。

生成隨機坐標的代碼如下:

public static int[][] createMineCoord() {
		//定義二維數組
		int[][] mineCoordArray = new int[Settings.MINE_NUM][Settings.MINE_SITE_NUM];
		Random random = new Random();
		//將生成的隨機坐標存入數組中
		for (int i = 0; i < Settings.MINE_NUM; i++) {
			for (int j = 0; j < Settings.MINE_SITE_NUM; j++) {
				//生成行坐標隨機數,並將其放入數組
				if (j == Settings.MINE_ROW_IN_ARRAY) {
					mineCoordArray[i][j] = random.nextInt(Settings.ROW_SIZE);
				}
				//生成列坐標隨機數,並將其放入數組
				if (j == Settings.MINE_COL_IN_ARRAY) {
					mineCoordArray[i][j] = random.nextInt(Settings.COL_SIZE);
				}
			}
		}
		
		return mineCoordArray;
	}

6.2 測試地雷生成

代碼如下:

import java.util.Random;

public class Test {
	public static void main(String[] args) {
		int[][][] gameData = init();
		for (int i = 0; i < 10; i++) {
			for (int j = 0; j < 10; j++) {
				System.out.print("(");
				for (int k= 0; k < 3; k++) {
					System.out.print(gameData[i][j][k]);
					if (k < 2) {
						System.out.print(",");
					}
				}
				System.out.print(")");
			}
			System.out.println();
		}
	}
	
	/**
	 * 初始化遊戲數據
	 * @return
	 */
	private static int[][][] init(){
		//創建大小為10*10*3的三維數組,並賦初值(默認初始值為0)
		int[][][] gameData = new int[10][10][3];
		
		//生成隨機的地雷坐標,並將其存入遊戲數據數組中
		int[][] mineCoordArray = createMineCoord();
		for (int[] mineCoord : mineCoordArray) {
			int row = mineCoord[0];
			int col = mineCoord[1];
			gameData[row][col][0] = 1;
		}

        //計算每格周圍地雷數並將其存入遊戲數據數組中
        
		return gameData;
	}
}

運行結果如下:

代碼運行數據

將其轉化為圖像形式就是:

隨機生成的地雷位置

6.3 計算每格周圍的地雷數目

思路:遍歷目標坐標點周圍的8個坐標點,每當發現一個地雷,則目標坐標點的遊戲數據數組中的統計地雷的數值加1。

實現代碼:

private void calcAroundNum() {
		
    for (int i = 0; i < Settings.ROW_SIZE; i++) {
        for (int j = 0; j < Settings.COL_SIZE; j++) {
            //遍歷當前坐標周圍的8個坐標
            for (int aroundRow = i-1; aroundRow <= i+1; aroundRow++) {
                //行號超范圍則跳過
                if (aroundRow < 0 || aroundRow > Settings.ROW_SIZE-1) {
                    continue;
                }
                for (int aroundCol = j-1; aroundCol <= j+1; aroundCol++) {
                    //列號超范圍則跳過
                    if (aroundCol < 0 || aroundCol > Settings.COL_SIZE-1) {
                        continue;
                    }
                    //排除本身坐標點
                    if ((gameData[aroundRow][aroundCol][Settings.MINE_DATA] == Settings.IS_MINE) && (!(aroundRow == i && aroundCol == j))) {
                        gameData[i][j][Settings.AROUND_MINE_DATA] += 1;
                    }
                }
            }
        }
    }

}

測試運行結果如下:

計算周邊地雷數

將其轉換為圖像表示:

周邊地雷圖示

7.遊戲運行畫面

7.1 踩中地雷

第一、二版:

踩雷

第三版:

踩雷2.0

7.2 通關遊戲

第一、二版:

通關

第三版:

通關2.0

到此這篇關於利用java開發丐版掃雷遊戲的文章就介紹到這瞭,更多相關java開發掃雷遊戲內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀:

    None Found