1.简介

学了几周的Java,闲来无事,写个乞丐版的扫雷,加强一下Java基础知识。

2.编写过程

编写这个游戏,一共经历了三个阶段,编写了三个版本的游戏代码。

第一版:完成了扫雷游戏的基本雏形,实现了游戏的基本功能,游戏运行在cmd黑窗口中,以字符绘制游戏界面,无图形化窗口,通过控制台输入字符完成游戏控制。代码放置在一个java文件中,代码的可读性以及可扩展性都比较差。

第二版:在第一版实现基本功能的基础之上,对游戏代码进行重构,根据各部分的功能创建多个类,增加代码注释,提高代码的可读性以及可扩展性。

第三版:在第二版重构代码的基础之上给游戏增加了图形化界面,将用户从控制台输入命令控制游戏变为通过鼠标左右键点击操作控制游戏。

3.游戏运行逻辑

游戏运行逻辑图(第一版代码):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jhfHYnSP-1618903934071)(https://upload-images.jianshu.io/upload_images/25993207-3b156359dd849db3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

游戏运行逻辑图(第二版代码):

以上两个游戏流程图的运行是建立在从控制台读取数据的基础之上的,两者的执行逻辑大体相同,其本质区别在于修改游戏数据的时机不相同。前者是在通关判断之前修改数据,后者实在通关判断之后。两者在运行期间并没有什么区别,但是当玩家完成扫雷之后最后的画面打印就会出现问题,即游戏画面中最后一个进行操作的坐标点的字符的显示状态,在发生改变之前就会终止程序。通过对修改游戏数据以及通关判断这两个操作的执行顺序进行调整,即可修正这一显示错误。

游戏运行逻辑图(第三版代码):

这个运行流程图是基于第三版加了图形化界面之后的游戏代码,游戏控制流程与控制台输入控制的流程基本相同,只是将从控制台读取用户输入变成了监听用户的鼠标左键与右键的点击事件。并且,从控制台读取数据时要保证游戏结束前一直进行读取,因此需要设置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的布局管理器设置为GridLayoutframe.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方法,添加响应鼠标操作的逻辑代码*/@Overridepublic 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 踩中地雷

第一、二版:

第三版:

7.2 通关游戏

第一、二版:

第三版:

到此这篇关于利用java开发丐版扫雷游戏的文章就介绍到这了

利用java开发简易版扫雷游戏相关推荐

  1. c语言小游戏 精简_C语言开发简易版扫雷小游戏

    C 语言开发简易版扫雷小游戏 本文给大家分享的是一个使用 C 语言开发的命令行下的简易版扫雷小游戏, 本身没有什么太 多的技术含量, 只不过是笔者的处女作, 所以还是推荐给大家, 希望对大家学习 C ...

  2. c++简易版扫雷游戏

    c++简易版扫雷游戏,代码如下: #include<iostream> #include<cstdlib> #include<time.h> using names ...

  3. 【C语言】简易版扫雷游戏(play game)

    目录 扫雷游戏

  4. 利用python实现简易版的贪吃蛇游戏(面向python小白)

    前言 这篇文章主要给大家介绍了关于如何利用python实现简易版的贪吃蛇游戏的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学 ...

  5. JAVA实现简易版【斗地主】小游戏

    JAVA实现简易版[斗地主]小游戏 gitee项目源码链接:https://gitee.com/xzq25_com/playcardtest 效果展示:快来人机对战吧 一 二 三

  6. JavaEE + Oracle + Spring Boot + Mybatis + Maven 开发简易版银行管理系统(附源码)

    文章目录 JavaEE + Oracle + Spring Boot + Mybatis + Maven 开发简易版银行管理系统 一.系统介绍 二.开发步骤 三.系统展示 四.开发总结 五.数据库相关 ...

  7. Java语言实现的扫雷游戏(一)

    Java类库中提供了用于GUI的swing开发工具包,我们可以利用swing设计出一些简单的经典小游戏,如扫雷,推箱子,俄罗斯方块等.接下来我就简单分享一下用Java设计扫雷游戏的思路与过程. 首先, ...

  8. 【C语言初阶】——简易版·扫雷(9*9)【运行逻辑思维导图+细节讲解+源码】【初级】

    目录 一.扫雷游戏的运行逻辑 二.代码逻辑讲解+源码 1.打印一个简易的游戏开始菜单 2.创建数组储存数据并初始化数组 代码逻辑讲解 源码 3.布置雷 代码逻辑讲解 源码 4.排雷 代码逻辑 源码 三 ...

  9. 用Java开发贪食蛇小游戏

    用Java开发贪食蛇小游戏 一.实验内容 1.实现贪吃蛇游戏基本功能,屏幕上随机出现一个"食物",称为豆子,上下左右控制"蛇"的移动,吃到"豆子&qu ...

最新文章

  1. XGBoost参数调优完全指南(附Python代码)——转载
  2. 2017-2018-1 20162306 实验五实验报告
  3. 域名转发与域名解析有什么区别?
  4. 关于li标签之间的间隔如何消除!
  5. python之WEB开发:图片管理
  6. 进击的Android Hook 注入术《二》
  7. 最受欢迎中国技术博客? 我才狂写4周唉
  8. css div里引用em字体会变斜体_CSS文字加粗斜体[解决代码]
  9. windows eclipse python环境搭建
  10. 【QT】QT从零入门教程(十七):QT+OpenCV+VS 打包exe
  11. 没有web.xml如何使用 Maven打war包?
  12. Win10无法访问Ubuntu18.04的smb解决
  13. 《TCPIP详解》卷一系列解读
  14. echarts 柱状图颜色及渐变色设置
  15. c语言计算火车运行图调整暂停发售,详解铁路12306列车运行图调整,暂停发售
  16. GDAL综合整理--7:GDAL实用工具简介
  17. soul-网关实战(二)如何成为了Soul的贡献者
  18. MATLAB函数大全 .
  19. matlab 小波的分解与重构
  20. WindowsAPI编程 简单的计算器

热门文章

  1. WPS表格 - 数字累加技巧总结
  2. 翻译音频的软件有哪些?将这几个可以翻译音频的软件分享给你
  3. php中平方代码_php 做出平方代码,用类来实现的接口,初学者请大侠们出手啊。...
  4. 中国石油大学计算机学院在哪个校区,中国石油大学(华东)有几个校区及校区地址 哪个校区最好...
  5. 如何把PDF转换成Word文档?这几种方法看一遍就会
  6. 测试代码运行时write to txt格式
  7. TSC TTP-342M Pro条码打印机的调用打印代码
  8. python数学计算函数_python之数学函数篇
  9. Java中一维数组的内存解析(浅显易懂,新手必看)
  10. Android Adapter的使用