1.簡介
學了幾周的Java,閑來無事,寫個乞丐版的掃雷,加強一下Java基礎知識。
2.編寫過程
編寫這個游戲,一共經(jīng)歷了三個階段,編寫了三個版本的游戲代碼。
第一版:完成了掃雷游戲的基本雛形,實現(xiàn)了游戲的基本功能,游戲運行在cmd黑窗口中,以字符繪制游戲界面,無圖形化窗口,通過控制臺輸入字符完成游戲控制。代碼放置在一個java文件中,代碼的可讀性以及可擴展性都比較差。
第二版:在第一版實現(xiàn)基本功能的基礎之上,對游戲代碼進行重構,根據(jù)各部分的功能創(chuàng)建多個類,增加代碼注釋,提高代碼的可讀性以及可擴展性。
第三版:在第二版重構代碼的基礎之上給游戲增加了圖形化界面,將用戶從控制臺輸入命令控制游戲變?yōu)橥ㄟ^鼠標左右鍵點擊操作控制游戲。
3.游戲運行邏輯
游戲運行邏輯圖(第一版代碼):
游戲運行邏輯圖(第二版代碼):
以上兩個游戲流程圖的運行是建立在從控制臺讀取數(shù)據(jù)的基礎之上的,兩者的執(zhí)行邏輯大體相同,其本質區(qū)別在于修改游戲數(shù)據(jù)的時機不相同。前者是在通關判斷之前修改數(shù)據(jù),后者實在通關判斷之后。兩者在運行期間并沒有什么區(qū)別,但是當玩家完成掃雷之后最后的畫面打印就會出現(xiàn)問題,即游戲畫面中最后一個進行操作的坐標點的字符的顯示狀態(tài),在發(fā)生改變之前就會終止程序。通過對修改游戲數(shù)據(jù)以及通關判斷這兩個操作的執(zhí)行順序進行調整,即可修正這一顯示錯誤。
游戲運行邏輯圖(第三版代碼):
這個運行流程圖是基于第三版加了圖形化界面之后的游戲代碼,游戲控制流程與控制臺輸入控制的流程基本相同,只是將從控制臺讀取用戶輸入變成了監(jiān)聽用戶的鼠標左鍵與右鍵的點擊事件。并且,從控制臺讀取數(shù)據(jù)時要保證游戲結束前一直進行讀取,因此需要設置while(true)循環(huán)以進行實現(xiàn),游戲結束使用break跳出循環(huán)。而在圖形化界面中,使用的是事件監(jiān)聽器,事件監(jiān)聽器在游戲結束前持續(xù)監(jiān)聽用戶的鼠標點擊事件,在游戲結束的彈窗彈出的同時移除監(jiān)聽器,結束鼠標對游戲的控制。
4.游戲相關數(shù)據(jù)存儲與讀取
以10x10x3的三維數(shù)組存儲每個坐標點的信息(包括行號、列號、是否是地雷、當前顯示的符號(未操作?、插旗#、地雷*)、周圍地雷數(shù))。
一維數(shù)組的下標表示行號,二維數(shù)組的下標表示列號,三維數(shù)組中存的第一個數(shù)據(jù)表示的是該位置是否是地雷(0-不是,1-是),第二個數(shù)據(jù)表示該位置當前顯示的符號(0-?,1-#,2-*,3-顯示地雷數(shù)),第三個數(shù)據(jù)表示該位置周圍的地雷數(shù)。例如:
array[0][9][0]=1;表示將第0行第9列設置為地雷
array[0][9][1]=1;表示將第0行第9列的顯示字符設置為'#'
array[0][9][2]=0;表示將第0行第9列位置周圍的地雷數(shù)設置為0
5.游戲代碼
5.1 第一版
第一版的游戲代碼寫在一個類中,主要的作用是實現(xiàn)基本的游戲功能。代碼如下:
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);
//初始化游戲數(shù)據(jù)
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;
}
//讀取控制臺數(shù)據(jù)并對游戲數(shù)據(jù)數(shù)組進行修改
gameData = readAndChangeData(input,gameData);
}
}
/**
* 打印提示信息
*/
private static void showInfo() {
printBlank(25);
System.out.println("*******************************************************
"
+ " 游戲信息
"
+ "游戲名稱:掃雷
"
+ "游戲版本:1.0
"
+ "游戲操作:1.輸入行號及列號來選中要翻開的'?'進行操作,可
"
+ " 以選擇插旗(#)或者直接翻開.
"
+ " 2.如果翻開'*'則表示地雷,則游戲結束;如果翻開
"
+ " 的是數(shù)字,則表示該格周圍的地雷數(shù).
"
+ " 3.標記出全部地雷,并且沒有'?',則闖關成功,游戲
"
+ " 結束.
"
+ "*******************************************************
");
}
/**
* 打印游戲框
*/
private static void showWin(int[][][] gameData) {
System.out.println(" 0 1 2 3 4 5 6 7 8 9
"
+ " ***********************");
//遍歷游戲框中的每個坐標,讀取并打印顯示符號
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() {
//定義二維數(shù)組
int[][] mineCoordArray = new int[20][2];
Random random = new Random();
//將生成的隨機坐標存入數(shù)組中
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 2; j++) {
//生成0~9范圍內(nèi)的隨機數(shù)
int randomNumber = random.nextInt(10);
mineCoordArray[i][j] = randomNumber;
}
}
return mineCoordArray;
}
/**
* 初始化游戲數(shù)據(jù)
*/
private static int[][][] init(){
//創(chuàng)建大小為10*10*3的三維數(shù)組(默認初始值為0)
int[][][] gameData = new int[10][10][3];
//生成隨機的地雷坐標,并將其存入游戲數(shù)據(jù)數(shù)組中
int[][] mineCoordArray = createMineCoord();
for (int[] mineCoord : mineCoordArray) {
int row = mineCoord[0];
int col = mineCoord[1];
gameData[row][col][0] = 1;
}
//計算每格周圍地雷數(shù)并將其存入游戲數(shù)據(jù)數(shù)組中
//循環(huán)遍歷每個坐標
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;
}
/**
* 從控制臺讀取數(shù)據(jù),并對游戲的數(shù)據(jù)數(shù)組進行修改
* @param input
*/
private static int[][][] readAndChangeData(Scanner input,int[][][] gameData) {
//定義在循環(huán)外部,以方便后續(xù)使用
int row;
int col;
printBlank(12);
//讀取輸入
//設置循環(huán)來讀取行號,當輸入的行號不在范圍內(nèi)時,會一直提示玩家
while (true) {
System.out.print("請輸入行號:");
row = input.nextInt();
if (row >= 0 && row <= 9) {
break;
} else {
System.out.println("輸入的行號不符合規(guī)范!");
}
}
//設置循環(huán)來讀取列號,當輸入的行號不在范圍內(nèi)時,會一直提示玩家
while(true) {
System.out.print("請輸入列號:");
col = input.nextInt();
if (col >= 0 && col <= 9) {
break;
} else {
System.out.println("輸入的列號不符合規(guī)范!");
}
}
//設置循環(huán),防止玩家輸入不能識別的字符
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;
}
}
//修改數(shù)據(jù)
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")) {
//如果翻開的不是炸彈,則顯示其周圍地雷數(shù)
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) {
//坐標點總數(shù)
int totalSite = 10 * 10;
//統(tǒng)計地雷數(shù)與非地雷數(shù)
int mineSigned = 0;
int noMineOpen = 0;
//遍歷游戲數(shù)據(jù)數(shù)組
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 第二版
這一版的代碼實在第一版的基礎上對代碼進行重構,根據(jù)功能對代碼進行分類,將其放入不同的類中,增加代碼的可讀性與可維護性、可擴展性。代碼一共分為五個類:主程序類、設置類、地雷類、控制類、顯示類。各部分代碼各司其職,共同作用,共同完成游戲運行。
主程序類,游戲程序入口:
import java.util.Scanner;
/**
* 掃雷游戲
* @author zjl
*
*/
public class MineSweeper {
/**
* 游戲運行主程序
* @param args
*/
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
//初始化游戲數(shù)據(jù)
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;
}
//讀取控制臺數(shù)據(jù)并對游戲數(shù)據(jù)數(shù)組進行修改
GameControl.readAndChangeData(input,gameData);
}
}
}
設置類,游戲相關設置數(shù)據(jù):
/**
* 定義游戲初始數(shù)據(jù)的類
* @author zjl
*
*/
public class Settings {
//定義游戲界面參數(shù)
/**
* 游戲界面的行數(shù)
*/
public static final int ROW_SIZE = 10;
/**
* 游戲界面的列數(shù)
*/
public static final int COL_SIZE = 10;
/**
* 兩個游戲界面之間的默認空白行數(shù)
*/
public static final int DEFAULT_BLANK = 20;
/**
* 地雷數(shù)
*/
public static final int MINE_NUM = 20;
/**
* 確定地雷位置所需要的坐標數(shù),由于是在平面內(nèi),所以只需要設置橫縱坐標,值為2
*/
public static final int MINE_SITE_NUM = 2;
/**
* 地雷行坐標在地雷數(shù)組中的下標
*/
public static final int MINE_ROW_IN_ARRAY = 0;
/**
* 地雷列坐標在地雷數(shù)組中的下標
*/
public static final int MINE_COL_IN_ARRAY = 1;
/**
* 每個坐標點中存儲數(shù)據(jù)的數(shù)組的大小
*/
public static final int DATA_SIZE = 3;
//定義每個坐標點中存儲數(shù)據(jù)的數(shù)組中數(shù)值的含義
/**
* 數(shù)組中存放地雷信息的位置
*/
public static final int MINE_DATA = 0;
/**
* 表示不是地雷,為默認值
*/
public static final int IS_NOT_MINE = 0;
/**
* 表示是地雷
*/
public static final int IS_MINE = 1;
/**
* 數(shù)組中存放符號信息的位置
*/
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;
/**
* 表示當前位置已翻開,即應該顯示當前位置的地雷數(shù)
*/
public static final int MINE_NUM_SIGN_DATA = 3;
/**
* 數(shù)組中存放坐標點周圍地雷數(shù)的位置
*/
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碼表中整數(shù)48~57代表字符0~9,設置一個增量值,將數(shù)字轉換為字符
*/
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 = "**************************************************
"
+ " 游戲信息
"
+ "游戲名稱:掃雷
"
+ "游戲版本:2.0
"
+ "游戲操作:1.輸入行號及列號來選中要翻開的'?'進行操作,可
"
+ " 以選擇插旗(#)或者直接翻開.
"
+ " 2.如果翻開'*'則表示地雷,則游戲結束;如果翻開
"
+ " 的是數(shù)字,則表示該格周圍的地雷數(shù).
"
+ " 3.標記出全部地雷,并且沒有'?',則闖關成功,游戲
"
+ " 結束.
"
+ "**************************************************
";
}
地雷類,生成隨機的地雷坐標數(shù)據(jù):
import java.util.Random;
/**
* 有關地雷的類
* @author zjl
*
*/
public class Mine {
/**
* 隨機生成地雷坐標
*/
public static int[][] createMineCoord() {
//定義二維數(shù)組
int[][] mineCoordArray = new int[Settings.MINE_NUM][Settings.MINE_SITE_NUM];
Random random = new Random();
//將生成的隨機坐標存入數(shù)組中
for (int i = 0; i < Settings.MINE_NUM; i++) {
for (int j = 0; j < Settings.MINE_SITE_NUM; j++) {
//生成行坐標隨機數(shù),并將其放入數(shù)組
if (j == Settings.MINE_ROW_IN_ARRAY) {
mineCoordArray[i][j] = random.nextInt(Settings.ROW_SIZE);
}
//生成列坐標隨機數(shù),并將其放入數(shù)組
if (j == Settings.MINE_COL_IN_ARRAY) {
mineCoordArray[i][j] = random.nextInt(Settings.COL_SIZE);
}
}
}
return mineCoordArray;
}
}
控制類,控制游戲進程,以及游戲數(shù)據(jù):
import java.util.Scanner;
/**
* 關于游戲相關控制的類
* @author zjl
*
*/
public class GameControl {
/**
* 初始化游戲數(shù)據(jù)
*/
public static int[][][] init(){
//創(chuàng)建存儲游戲相關數(shù)據(jù)的三維數(shù)組(默認初始值為0)
int[][][] gameData = new int[Settings.ROW_SIZE][Settings.COL_SIZE][Settings.DATA_SIZE];
//生成隨機的地雷坐標,并將其存入游戲數(shù)據(jù)數(shù)組中
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;
}
//計算每格周圍地雷數(shù)并將其存入游戲數(shù)據(jù)數(shù)組中
//循環(huán)遍歷每個坐標
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;
}
/**
* 從控制臺讀取數(shù)據(jù),并對游戲的數(shù)據(jù)數(shù)組進行修改
* @param input
*/
public static void readAndChangeData(Scanner input,int[][][] gameData) {
//定義在循環(huán)外部,以方便后續(xù)使用
int row;
int col;
//讀取輸入
//設置循環(huán)來讀取行號,當輸入的行號不在范圍內(nèi)時,會一直提示玩家
while (true) {
System.out.print("請輸入行號:");
row = input.nextInt();
if (row >= 0 && row <= Settings.ROW_SIZE-1) {
break;
} else {
System.out.println("輸入的行號不符合規(guī)范!");
}
}
//設置循環(huán)來讀取列號,當輸入的行號不在范圍內(nèi)時,會一直提示玩家
while(true) {
System.out.print("請輸入列號:");
col = input.nextInt();
if (col >= 0 && col <= Settings.COL_SIZE-1) {
break;
} else {
System.out.println("輸入的列號不符合規(guī)范!");
}
}
//設置循環(huán),防止玩家輸入不能識別的字符
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;
}
}
//修改數(shù)據(jù)
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)) {
//如果翻開的不是炸彈,則顯示其周圍地雷數(shù)
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) {
//坐標點總數(shù)
int totalSite = Settings.ROW_SIZE * Settings.COL_SIZE;
//統(tǒng)計地雷數(shù)與非地雷數(shù)
int mineSigned = 0;
int noMineOpen = 0;
//遍歷游戲數(shù)據(jù)數(shù)組
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++;
}
}
}
//當翻開的的坐標數(shù)加上標記的地雷數(shù)等于坐標點總數(shù)的時候,返回true表示可以結束游戲
if (totalSite == (noMineOpen + mineSigned)) {
return true;
}
//條件不滿足,游戲繼續(xù)
return false;
}
}
顯示類,對游戲的相關畫面進行打?。?/p>
/**
* 展示游戲相關畫面的類
* @author zjl
*
*/
public class Show {
/**
* 打印提示信息
*/
public static void gameInfo() {
//打印空白行,作用是使展現(xiàn)在控制臺的圖形刷新
printSign(Settings.DEFAULT_BLANK,"
");
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, "
*");
printSign(Settings.COL_SIZE+1, "**");
printSign(1, "
");
//遍歷游戲框中的每個坐標,讀取并打印顯示符號
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:
//將數(shù)組中存的整型數(shù)值通過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, "*
");
}
}
5.3 第三版
在第二版的基礎上,去除了顯示類,增加了圖片類、圖形界面類、事件監(jiān)聽器類。
游戲運行主程序類:
/**
* 掃雷游戲主程序類
* @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];
//創(chuàng)建游戲控制類對象
GameDataController controller = new GameDataController(gameData);
//初始化游戲數(shù)據(jù)
controller.init();
//繪制游戲界面
new Graphic(controller);
}
}
設置類,提供游戲運行相關這是數(shù)據(jù):
/**
* 定義游戲初始數(shù)據(jù)的類
* @author zjl
*
*/
public class Settings {
//定義游戲界面參數(shù)
/**
* 游戲界面的行數(shù)
*/
public static final int ROW_SIZE = 10;
/**
* 游戲界面的列數(shù)
*/
public static final int COL_SIZE = 10;
/**
* 地雷數(shù)
*/
public static final int MINE_NUM = 20;
/**
* 確定地雷位置所需要的坐標數(shù),由于是在平面內(nèi),所以只需要設置橫縱坐標,值為2
*/
public static final int MINE_SITE_NUM = 2;
/**
* 地雷行坐標在地雷數(shù)組中的下標
*/
public static final int MINE_ROW_IN_ARRAY = 0;
/**
* 地雷列坐標在地雷數(shù)組中的下標
*/
public static final int MINE_COL_IN_ARRAY = 1;
/**
* 每個坐標點中存儲數(shù)據(jù)的數(shù)組的大小
*/
public static final int DATA_SIZE = 3;
//定義每個坐標點中存儲數(shù)據(jù)的數(shù)組中數(shù)值的含義
/**
* 數(shù)組中存放地雷信息的位置
*/
public static final int MINE_DATA = 0;
/**
* 表示不是地雷,為默認值
*/
public static final int IS_NOT_MINE = 0;
/**
* 表示是地雷
*/
public static final int IS_MINE = 1;
/**
* 數(shù)組中存放符號信息的位置
*/
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;
/**
* 表示當前位置已翻開,即應該顯示當前位置的地雷數(shù)
*/
public static final int MINE_NUM_SIGN_DATA = 3;
/**
* 數(shù)組中存放坐標點周圍地雷數(shù)的位置
*/
public static final int AROUND_MINE_DATA = 2;
//定義游戲框尺寸數(shù)據(jù)
/**
* 圖片邊長
*/
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;
/**
* 游戲網(wǎng)格線的寬度
*/
public static final int BORDER_WIDTH = 1;
/**
* 游戲窗口標題欄高度
*/
public static final int TITLE_HEIGHT = 23;
//彈窗數(shù)據(jù)
/**
* 彈窗標題
*/
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");
/**
* 這是一個用于返回數(shù)字圖片的靜態(tài)方法,
* 通過傳入的參數(shù)來獲取表示對應數(shù)字的圖片,
* 返回的圖片上的數(shù)字表示某位置周圍存在的地雷數(shù)
* @param mineNum-int類型,表示傳入地雷數(shù)量的參數(shù)
* @return image-ImageIcon類型的返回值,返回的圖片上的數(shù)字與參數(shù)mineNum對應
*/
public static ImageIcon getImageByNum(int mineNum) {
ImageIcon image = new ImageIcon("img\"+mineNum+".png");
return image;
}
}
地雷類,生成游戲中的類的相關數(shù)據(jù):
import java.util.Random;
/**
* 這是一個用于生成隨機地雷坐標的類。
* 由于生成的是偽隨機數(shù),因此生成的地雷坐標可能重復,所以實際游戲中的地雷數(shù)量并不固定。
* @author zjl
*
*/
public class Mine {
/**
* 隨機生成坐標數(shù)據(jù),為游戲提供隨機的地雷坐標數(shù)據(jù)。
* @return mineCoordArray-int類型的二維數(shù)組,存儲的是生成的地雷的坐標數(shù)據(jù)
*/
public static int[][] createMineCoord() {
//定義二維數(shù)組
int[][] mineCoordArray = new int[Settings.MINE_NUM][Settings.MINE_SITE_NUM];
Random random = new Random();
//將生成的隨機坐標存入數(shù)組中
for (int i = 0; i < Settings.MINE_NUM; i++) {
for (int j = 0; j < Settings.MINE_SITE_NUM; j++) {
//生成行坐標隨機數(shù),并將其放入數(shù)組
if (j == Settings.MINE_ROW_IN_ARRAY) {
mineCoordArray[i][j] = random.nextInt(Settings.ROW_SIZE);
}
//生成列坐標隨機數(shù),并將其放入數(shù)組
if (j == Settings.MINE_COL_IN_ARRAY) {
mineCoordArray[i][j] = random.nextInt(Settings.COL_SIZE);
}
}
}
return mineCoordArray;
}
}
游戲控制類,提供用于游戲控制的相關方法:
import javax.swing.JLabel;
/**
* 這是一個用于游戲數(shù)據(jù)控制的類,
* @author zjl
*
*/
public class GameDataController {
/**
* 私有的成員變量,用于存儲在構造方法中接收到的游戲數(shù)據(jù)
*/
private int[][][] gameData;
/**
* 這是本類的一個有參構造方法,通過傳入游戲數(shù)據(jù)來構造一個游戲數(shù)據(jù)控制器
* @param gameData-存儲游戲相關數(shù)據(jù)的三維數(shù)組
*/
public GameDataController(int[][][] gameData) {
this.gameData = gameData;
}
/**
* 初始化游戲數(shù)據(jù)
*/
public void init(){
//將地雷數(shù)據(jù)存入三維游戲數(shù)組中
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;
}
//計算每格周圍地雷數(shù)并將其存入游戲數(shù)據(jù)數(shù)組中
calcAroundNum();
}
/**
* 游戲通關判斷
* @return 返回boolean類型的true或false,true表示游戲通關
*/
public boolean missionAccomplished() {
//坐標點總數(shù)
int totalSite = Settings.ROW_SIZE * Settings.COL_SIZE;
//統(tǒng)計地雷數(shù)與非地雷數(shù)
int mineSigned = 0;
int noMineOpen = 0;
//遍歷游戲數(shù)據(jù)數(shù)組
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++;
}
}
}
//當翻開的的坐標數(shù)加上標記的地雷數(shù)等于坐標點總數(shù)的時候,返回true表示可以結束游戲
if (totalSite == (noMineOpen + mineSigned)) {
return true;
}
//條件不滿足,游戲繼續(xù)
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 {
//如果當前位置未被翻開,則翻開當前位置,修改游戲數(shù)據(jù)及顯示圖片
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) {
//如果當前位置未被翻開,則修改相應數(shù)據(jù),并將其顯示為插旗
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) {
//如果該位置已被插旗,則修改相應數(shù)據(jù),并將其恢復初始狀態(tài)
gameData[y][x][Settings.SIGN_DATA] = Settings.INIT_SIGN_DATA;
labels[y][x].setIcon(null);
}
}
/**
* 計算每個位置周圍的地雷數(shù),并將算出的結果存入到三維游戲數(shù)組中
*/
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類型的的靜態(tài)屬性frame
*/
private static JFrame frame;
/**
* 定義GameListener類型的游戲事件監(jiān)聽器gameListener
*/
private static GameListener gameListener;
/**
* 定義用于存儲JLabel類型數(shù)據(jù)的二維數(shù)組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);
}
/**
* 這是該類的一個有參構造方法,用于創(chuàng)建Graphic對象。
* 該構造方法調用本類中的draw()方法,繪制游戲的圖形化界面
* @param controller-接收主函數(shù)中傳入的游戲參數(shù)控制器對象,后續(xù)將其傳遞到事件監(jiān)聽器中,使監(jiān)聽器能夠修改游戲數(shù)據(jù)
*/
public Graphic(GameDataController controller) {
this.controller = controller;
draw();
}
/**
* 繪制游戲的圖形化界面,并在游戲窗口上添加事件監(jiān)聽器
*/
private void draw() {
//通過循環(huán)創(chuàng)建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));
}
}
//創(chuàng)建事件監(jiān)聽器,監(jiān)聽鼠標點擊在frame上的位置,并將監(jiān)聽器添加到frame上
gameListener = new GameListener(labels, controller);
frame.addMouseListener(gameListener);
}
/**
* 繪制游戲結束時的彈窗。
* 根據(jù)傳入的參數(shù)判斷游戲是因為踩到地雷而結束還是因為通關而結束,從而繪制不同效果的彈窗
* @param result-boolean類型的參數(shù),表示游戲是因為通關結束還是因為踩雷結束
*/
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;
}
//彈窗出現(xiàn)表示游戲結束,此時應移除窗體上的事件監(jiān)聽器
frame.removeMouseListener(gameListener);
//根據(jù)相關參數(shù)繪制彈窗
option = JOptionPane.showConfirmDialog(null, message, Settings.DIALOG_TITLE, JOptionPane.CANCEL_OPTION,JOptionPane.INFORMATION_MESSAGE,image);
/* 根據(jù)彈窗上的按鈕點擊結果判斷是否關閉游戲退出程序。
* 只有在點擊確定時才會結束程序,點擊取消并不會推出游戲,
* 而是停留在游戲結束時的畫面,但是不能進行游戲操作,
* 點擊關閉窗口即可退出程序
*/
if (option != JOptionPane.CANCEL_OPTION) {
System.exit(0);
}
}
}
事件監(jiān)聽器類,用于提供監(jiān)聽鼠標點擊事件的監(jiān)聽器:
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
/**
* 定義游戲相關的事件監(jiān)聽器
* @author zjl
*
*/
public class GameListener extends MouseAdapter {
/**
* 私有的成員變量,用以接收構造事件監(jiān)聽器時傳入的JLabel數(shù)組
*/
private JLabel[][] labels;
/**
* 私有的成員變量,用以接收構造事件監(jiān)聽器時傳入的游戲數(shù)據(jù)控制器對象
*/
private GameDataController controller;
/**
* 這是本類一個有參構造方法,用于根據(jù)傳入的參數(shù)構造事件監(jiān)聽器對象
* @param labels-這是一個JLabel數(shù)組,為后續(xù)改變相應的圖像顯示提供容納圖片的JLabel組件
* @param controller-游戲數(shù)據(jù)控制器對象,用于改變相應游戲數(shù)據(jù)
*/
public GameListener(JLabel[][] labels, GameDataController controller) {
this.labels = labels;
this.controller = controller;
}
/**
* 重寫MouseAdapter類中的mouseClicked方法,添加響應鼠標操作的邏輯代碼
*/
@Override
public void mouseClicked(MouseEvent e) {
//對鼠標點擊點的坐標進行計算可得到label在數(shù)組中的下標
int x = (e.getX()-Settings.BORDER_WIDTH)/Settings.IMAGE_SIZE;
int y = (e.getY()-Settings.TITLE_HEIGHT)/Settings.IMAGE_SIZE;
//區(qū)分鼠標左擊右擊事件
if (e.getButton() == MouseEvent.BUTTON1) {//鼠標左擊,進行的操作為翻開當前位置
controller.leftClick(x, y, labels);
} else if (e.getButton() == MouseEvent.BUTTON3) {//鼠標右擊,進行的操作為插旗與取消插旗
controller.rightClick(x, y, labels);
}
//通關判斷
if (controller.missionAccomplished()) {
//通關則顯示彈窗并移除監(jiān)聽器
Graphic.showDialog(true);
}
}
}
6.部分代碼思路
6.1 生成隨機的地雷坐標
創(chuàng)建Random類的對象,使用相關方法,生成地雷行坐標與列坐標的隨機數(shù)值,使用二維數(shù)組存儲坐標點數(shù)據(jù)。由于沒有做去重處理,因此有概率生成多個相同的坐標,所以地雷數(shù)最多為設置的生成數(shù),最少為1(概率極低)。
生成隨機坐標的代碼如下:
public static int[][] createMineCoord() {
//定義二維數(shù)組
int[][] mineCoordArray = new int[Settings.MINE_NUM][Settings.MINE_SITE_NUM];
Random random = new Random();
//將生成的隨機坐標存入數(shù)組中
for (int i = 0; i < Settings.MINE_NUM; i++) {
for (int j = 0; j < Settings.MINE_SITE_NUM; j++) {
//生成行坐標隨機數(shù),并將其放入數(shù)組
if (j == Settings.MINE_ROW_IN_ARRAY) {
mineCoordArray[i][j] = random.nextInt(Settings.ROW_SIZE);
}
//生成列坐標隨機數(shù),并將其放入數(shù)組
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();
}
}
/**
* 初始化游戲數(shù)據(jù)
* @return
*/
private static int[][][] init(){
//創(chuàng)建大小為10*10*3的三維數(shù)組,并賦初值(默認初始值為0)
int[][][] gameData = new int[10][10][3];
//生成隨機的地雷坐標,并將其存入游戲數(shù)據(jù)數(shù)組中
int[][] mineCoordArray = createMineCoord();
for (int[] mineCoord : mineCoordArray) {
int row = mineCoord[0];
int col = mineCoord[1];
gameData[row][col][0] = 1;
}
//計算每格周圍地雷數(shù)并將其存入游戲數(shù)據(jù)數(shù)組中
return gameData;
}
}
運行結果如下:
將其轉化為圖像形式就是:
6.3 計算每格周圍的地雷數(shù)目
思路:遍歷目標坐標點周圍的8個坐標點,每當發(fā)現(xiàn)一個地雷,則目標坐標點的游戲數(shù)據(jù)數(shù)組中的統(tǒng)計地雷的數(shù)值加1。
實現(xiàn)代碼:
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 踩中地雷
第一、二版:
第三版:
7.2 通關游戲
第一、二版:
第三版:
關于使用Java來實現(xiàn)一款簡單的掃雷小游戲的文章就介紹到此結束了,有興趣的小伙伴可以嘗試一下。如果還想要了解更多關于Java相關的小游戲制作,請多多關注W3Cschool其他相關內(nèi)容的文章如今。