目录
一、问题描述
二、逻辑结构设计
三、存储结构设计
三、主要操作设计
四、技术难点与解决方法
五、实现与展示
六、详细代码
七、游戏内图片
一、问题描述
设计实现经典扫雷游戏,要求如下:
(1) 分初级、中级和高级三个级别,扫雷英雄榜存储每个级别的最好成绩,即挖出所有地雷
且用时最少者。
(2) 选择级别后出现相对应级别的扫雷区域,用户使用鼠标左键单击雷区中任一方块便启动
计时器。
(3) 单击方块,若所揭方块下有雷,则 Game Over;若所揭方块下无雷,则显示一个数字,
该数字代表方块周围 8 个方块中共有多少颗雷。如果数字为 0,则程序自动揭开方块周
围方块,直到找到非 0 数字的方块。
(4) 用户可以右键单击标记雷,无论用户标记是否正确,程序都将显示剩余雷数少一。
(5) 胜利后,用时少于排行榜最末成绩,则弹窗提示保存成绩
[测试数据]
参照原版设计:win7 或者 winXP 系统下自带扫雷游戏,或者扫雷游戏网页版 -
Minesweeper
[实现提示]
可能用到数组、排序、递归等。
二、逻辑结构设计
图2.1扫雷操作流程图
首先GameStart类中进行程序,调用MineSweeping的构造方法来绘制窗口与基本组件,然后在MineSweeping类中调用GamePanel来绘制雷区,绘制雷区时用了两层for循环遍历,并用if方法判断地雷是否被埋在重复的位置。绘制完毕后便可以开始游戏。
用户点击任意格子后会先进行判断,若为雷,游戏失败。若为数字,不会翻开周围的格子。若为空,则会递归调用翻开周围3*3的格子。
若游戏失败,则结束游戏,用户可以点击顶部的笑脸重新开始游戏。若胜利,则输入名称,并生成一个Hero对象存入到ArrayList当中,排序并显示英雄榜。
三、存储结构设计
该课程设计的类主要包含两个大部分。一个是绘制扫雷地图与组件的类,另一个是存储获胜用户信息的类。在绘制扫雷地图中,包括GamePanel、GameUtils、MineSweeping类等。在存储用户信息及英雄榜排名中,包括Hero、HeroDialog。
图3.1绘制地图uml类图
(1)GameStart是整个游戏开始的入口,从这里开始运行程序。
(2)其中GameUtils存储了绘制扫雷地图时所需要的变量,比如地图的宽、高,地的偏移量、地雷的数量等。并将这些变量全部设置为静态变量,以此能在不同类中很好的调用。
(3) GamePanel绘制雷区的部分,利用randomBomb()和writeNumber()来随机生成地雷与数字,open()与subOpen()来绘制地雷翻开的动作。因为雷区是一个矩阵,因此将地雷与按钮存储到一个二维数组当中,同时在此类中判断游戏是否胜利。
(4)MineSweeping绘制了地图其他组件,将笑脸,倒计时,剩余雷数以及排行榜添加到窗口当中。并对调用GamePanel类,将扫雷地图进行组装,最后实现出扫雷的效果。
图3.2 排行榜制作uml类图
(5)当用户在VistoryDialog中输入名称后,用Hero类来new一个对象,因为考虑到后续的插入操作,在这里使用数组的效率会更高一些,因此将这些对象存入到ArrayList当中
三、主要操作设计
扫雷窗口与雷区生成后,顶部有三个按钮,分别是难度选择,重新开始游戏以及查看排行榜。在游戏进行的任何时候都可以进行难度选择,或是重新开始游戏。用户点击任意格子后会先进行判断,若为雷或数字,不会翻开周围的格子。若为空,则会递归调用翻开周围3*3的格子。数字代表周围3*3的地区有N个雷。若点到雷,游戏失败,若将除了雷以外的其他格子都翻开的话,则游戏成功。输入名称,显示排行榜。
四、技术难点与解决方法
技术难点:
①扫雷时翻开的操作比较难以实现,其一是要判断点开格子下面是否可以继续翻开其他周围的格子,其二是要如何实现翻开其周围的格子
②计时操作难以实现,其要求每秒都要改变秒数。其次在每次重新开局以及选择难度后倒计时都要重置,这一点也难以实现。
解决方法:
①首先用一个if语句进行判断,如果格子下面是数字或地雷的话,则不用继续翻开周围的格子。若格子下面为空,则利用递归算法遍历其周围3*3的格子,若还为空,则继续调用遍历。
②计时操作必须根据用户点下去的一瞬间开始计时。在这里必须要让计时操作与主程序分开开来,因此使用到了Java里面的多线程操作,将计时放到与主程序不一样的线程里面进行操作。这样就可以使计时操作与主程序同步进行。同时在Open()添加一个boolean类型的变量,记录按钮是否被点击,如果被点击,则开始计时。
五、实现与展示
图5.1游戏主要界面
游戏开始的主界面,顶部菜单显示五个部分,分别为计时、难度选择、重新开始按键、排行榜查看以及剩余雷(旗子)数显示。
图5.2难度选择
难度选择可以切换到不同的难度界面。初级是标准的9*9一共10个雷,中级是标准的16*16一共140个雷,高级是标准的16*40一共99个雷
图5.3中级与高级界面
图5.4游戏成功与失败展示
当游戏成功时,会跳出一个对话框,要求用户输入姓名。输入姓名后点击确定会录入到英雄榜当中,并进行排名显示英雄榜。当游戏失败时,点击确定按钮后可以选择顶部笑脸按钮以重新开始游戏。
图5.5游戏排行榜展示
排行榜会显示用户的昵称以及扫雷所花费的时间,并按照升序从上到下依次排列。
六、详细代码
public class GameStart {
public static void main(String[] args) {
/*
GameStart是整个游戏开始的入口,从这里开始运行程序。
*/
new MineSweeping();//调用MineSweeping绘制窗口框架与基本组件
}
}
import javax.swing.*;
public class Btn extends JButton {
public int i,j;
}
import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import java.awt.*;
import java.awt.event.MouseEvent;
public class GamePanel extends JPanel {
/*
GamePanel绘制雷区的部分,利用randomBomb()和writeNumber()来随机生成地雷与数字,
open()与subOpen()来绘制地雷翻开的动作。因为雷区是一个矩阵,因此将地雷与按钮存储到一个二维数组当中,同时在此类中判断游戏是否胜利。
*/
int count = 0;
public GamePanel() {
GameUtils.NUM_FLAGS = GameUtils.NUM_MINES;
GameUtils.label = new JLabel[GameUtils.MAP_H][GameUtils.MAP_W]; // 用于存储图案
GameUtils.isBomb = new boolean[GameUtils.MAP_H][GameUtils.MAP_W]; // 用于存储是否有地雷(true-有地雷,false-没有地雷)
GameUtils.buttons = new Btn[GameUtils.MAP_H][GameUtils.MAP_W];//绘制按钮
GameUtils.state = new int[GameUtils.MAP_H][GameUtils.MAP_W]; // 用于存储方格状态(0-未点击,1-已点击,2-未点击但周围有雷,3-插旗字)
MineSweeping.setMineNum(GameUtils.NUM_FLAGS);//调用该方法实现棋子数的修改
setLayout(null);
initLable();
randomBomb();
writeNumber();
drawBtn();
}
// 将地图底部初始化为一个个小格子,方便后续填充图片
public void initLable() {
for (int i = 0; i < GameUtils.MAP_H; i++) {
for (int j = 0; j < GameUtils.MAP_W; j++) {
JLabel l = new JLabel("", JLabel.CENTER);
l.setBounds(j * GameUtils.DELL_LENGTH,
i * GameUtils.DELL_LENGTH,
GameUtils.DELL_LENGTH,
GameUtils.DELL_LENGTH);//设置大小和位置
l.setBorder(BorderFactory.createLineBorder(Color.GRAY)); // 绘制方格边框
l.setOpaque(true);
l.setBackground(Color.lightGray);
this.add(l);
GameUtils.label[i][j] = l;//将lable标签放入一个个格子中整齐排布
GameUtils.label[i][j].setVisible(false);//可见性
}
}
}
//绘制地雷
private void randomBomb() {
for (int i = 0; i < GameUtils.NUM_MINES; i++) {
int rRow = (int) (Math.random() * GameUtils.MAP_H);
int rCol = (int) (Math.random() * GameUtils.MAP_W);
// 避免雷埋在相同的位置
if (GameUtils.isBomb[rRow][rCol]) {
i--;
}
GameUtils.label[rRow][rCol].setIcon(GameUtils.bomb);//设置图片
GameUtils.isBomb[rRow][rCol] = true;
}
}
// 绘制数字
private void writeNumber() {
for (int i = 0; i < GameUtils.MAP_H; i++) {
for (int j = 0; j < GameUtils.MAP_W; j++) {//前两村遍历宽和高
if (GameUtils.isBomb[i][j]) {
continue;//当前想绘制数字的点是否有雷如果没有雷继续遍历下一个格子
}
int bombCount = 0;
/* 寻找以自己为中心的九个格子中的地雷数 */
for (int k = -1; (k + i < GameUtils.MAP_H) && (k < 2); k++) {
//从这个格子周围3*3遍历
if (k + i < 0) {
continue;//判断是否越界
}
for (int c = -1; (c + j < GameUtils.MAP_W) && (c < 2); c++) {
if (c + j < 0) {
continue;//判断是否越界
}
if (GameUtils.isBomb[k + i][c + j]) {
bombCount++;//若有雷数字加一
}
}
if (bombCount > 0) {
GameUtils.state[i][j] = 2;//游戏=2意味这这个格子是数字
GameUtils.label[i][j].setText(String.valueOf(bombCount));//设置数字的图案
}
}
}
}
}
// 绘制按钮
private void drawBtn() {
for (int i = 0; i < GameUtils.MAP_H; i++) {
for (int j = 0; j < GameUtils.MAP_W; j++) {
//把按钮赋坐标值 把每一个btn按钮添加鼠标点击事件
Btn btn = new Btn();
btn.i = i;
btn.j = j;
btn.setBounds(j * GameUtils.DELL_LENGTH, i * GameUtils.DELL_LENGTH, GameUtils.DELL_LENGTH, GameUtils.DELL_LENGTH);
//设置大小和位置
this.add(btn);
GameUtils.buttons[i][j] = btn;
btn.addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
/* 左键点击翻开 */
if (e.getButton() == MouseEvent.BUTTON1) {
open(btn);
}
/* 右键点击插旗 */
if (e.getButton() == MouseEvent.BUTTON3) {
placeFlag(btn);
}
}
});
}
}
}
// 打开这个雷区
private void open(Btn b) {
// 如果踩雷
if (GameUtils.isBomb[b.i][b.j]) {
for (int k = 0; k < GameUtils.MAP_H; k++) {
for (int t = 0; t < GameUtils.MAP_W; t++) {
GameUtils.buttons[k][t].setVisible(false);
GameUtils.label[k][t].setVisible(true);
//把每一个格子都翻开
}
}
GameUtils.gameSate = -1;
JOptionPane.showMessageDialog(null, "您失败了", "游戏结束", JOptionPane.PLAIN_MESSAGE);
} else {
if (!GameUtils.isClick) {
MineSweeping.TimeClock timeClock = new MineSweeping.TimeClock();//点击一瞬间不是地雷就开始计时
timeClock.start();
GameUtils.isClick = true;
}
subOpen(b);
isVictory();
}
}
// 递归打开周边雷区
private void subOpen(Btn button) {
//不能打开的情况
if (GameUtils.isBomb[button.i][button.j] || GameUtils.state[button.i][button.j] == 1 || GameUtils.state[button.i][button.j] == 3) {
return;
}
/* 周围有雷的,只打开它 */
if (GameUtils.state[button.i][button.j] == 2) {
button.setVisible(false);
GameUtils.label[button.i][button.j].setVisible(true);
GameUtils.state[button.i][button.j] = 1;
count ++;
return;
}
/* 打开当前这个按钮 */
button.setVisible(false);
GameUtils.label[button.i][button.j].setVisible(true);
GameUtils.state[button.i][button.j] = 1;
count ++;
/* 递归检测周边八个按钮 */
for (int r = -1; (r + button.i < GameUtils.MAP_H) && (r < 2); ++r) {
if (r + button.i < 0) continue;
for (int c = -1; (c + button.j < GameUtils.MAP_W) && (c < 2); ++c) {
if (c + button.j < 0) {
continue;
}
if (r == 0 && c == 0) {
continue;
}
Btn newBtn = GameUtils.buttons[r + button.i][c + button.j];
subOpen(newBtn);
}
}
}
// 绘制插旗
private void placeFlag(Btn b) {
// 插过旗的,再点一次取消
if (GameUtils.state[b.i][b.j] == 3) {
if (GameUtils.label[b.i][b.j].getText() == "[0-9]") GameUtils.state[b.i][b.j] = 2;
else GameUtils.state[b.i][b.j] = 0;
b.setIcon(GameUtils.transparency);
++GameUtils.NUM_FLAGS;
MineSweeping.setMineNum(GameUtils.NUM_FLAGS);
} else if (GameUtils.NUM_FLAGS > 0) { // 剩余旗子数量等于0时则不能再插旗
b.setIcon(GameUtils.flag);
GameUtils.state[b.i][b.j] = 3;
--GameUtils.NUM_FLAGS;
MineSweeping.setMineNum(GameUtils.NUM_FLAGS);
}
if(GameUtils.NUM_FLAGS == 0){
boolean flagstate = true;
for(int i = 0;i < GameUtils.MAP_H; ++i){
for(int j = 0; j < GameUtils.MAP_W; ++j){
if (GameUtils.state[i][j] != 3 && GameUtils.isBomb[i][j]) flagstate = false;
}
}
}
}
// 失败显示所有地雷
public void showBomb() {
for (int r = 0; r < GameUtils.MAP_H; ++r) {
for (int c = 0; c < GameUtils.MAP_W; ++c) {
GameUtils.buttons[r][c].setVisible(false);
GameUtils.label[r][c].setVisible(true);
}
}
}
public void isVictory() {
if (count + GameUtils.NUM_MINES == GameUtils.MAP_W * GameUtils.MAP_H) {
GameUtils.gameSate = 1;
new VictoryDialog(GameUtils.GAME_TIME);
}
}
}
import javax.swing.*;
import java.util.ArrayList;
public class GameUtils {
/*
GameUtils存储了绘制扫雷地图时所需要的变量,比如地图的宽、高,地的偏移量、地雷的数量等。并将这些变量全部设置为静态变量,以此能在不同类中很好的调用。
*/
// 窗口的相关参数
static int MAP_W = 9;
static int MAP_H = 9;
static final int DELL_LENGTH = 20;
static final int OFFSET = 40;
static int GAME_TIME = 0;
// 游戏的相关状态
static JLabel[][] label;
static boolean[][] isBomb;
static Btn[][] buttons;
static int[][] state;//定义格子的状态 0-未点击,1-已点击,2-未点击但周围有雷,3-插旗字
static int gameSate;
static boolean isClick = false;
static ArrayList<Hero> heroesList = new ArrayList<>();
// 地雷与棋子的数量
static int NUM_MINES = 10;
static int NUM_FLAGS = 10;
// 游戏的相关图片
static ImageIcon flag = new ImageIcon("src/experimentFinal1/images/flag.jpg");
static ImageIcon bomb = new ImageIcon("src/experimentFinal1/images/bomb.jpg");
static ImageIcon transparency = new ImageIcon("src/experimentFinal1/images/transparency.png");
static ImageIcon face = new ImageIcon("src/experimentFinal1/images/face.jpg");
public static void addHero() {
heroesList.add(new Hero("张三", 45));
heroesList.add(new Hero("李四", 60));
heroesList.add(new Hero("王五", 30));
heroesList.add(new Hero("赵六", 51));
heroesList.add(new Hero("狗蛋", 33));
}
}
import java.io.Serializable;
// 该对象想要序列化或反序列化你需要实现Serializable接口,此接口为标记接口,不需要重写方法
public class Hero implements Serializable {
private String name = "";
private int time = 0;
public Hero(String name, int time) {
this.name = name;
this.time = time;
}
public String getName() {
return name;
}
public int getTime() {
return time;
}
}
package experimentFinal1;
import javax.swing.*;
public class HeroesDialog extends JFrame {
public HeroesDialog() /*throws IOException, ClassNotFoundException*/ {
this.setVisible(true);
this.setSize(200, 200);
this.setLocationRelativeTo(null);
insertSort();
String[] heroes = new String[GameUtils.heroesList.size()];
for (int i = 0; i < GameUtils.heroesList.size(); i++) {
heroes[i] = GameUtils.heroesList.get(i).getName()
+ " "
+ GameUtils.heroesList.get(i).getTime();
}
JLabel label = new JLabel();
label.setText("英雄排行榜");
JList<String> list = new JList<>(heroes);
JPanel panel = new JPanel();
panel.add(label);
panel.add(new JScrollPane(list));
this.add(panel);
}
private void insertSort() {
for (int i = 1; i < GameUtils.heroesList.size(); i++) {
for (int j = i; j > 0; j--) {
if (GameUtils.heroesList.get(j - 1).getTime() > GameUtils.heroesList.get(j).getTime()) {
Hero temp = GameUtils.heroesList.get(j - 1);
GameUtils.heroesList.set(j - 1, GameUtils.heroesList.get(j));
GameUtils.heroesList.set(j, temp);
} else {
break;
}
}
}
}
}
package experimentFinal1;
public class LevelSelect {
public LevelSelect(String item) {
switch (item) {
case "初级":
GameUtils.NUM_MINES = 10;
GameUtils.MAP_W = 9;
GameUtils.MAP_H = 9;
break;
case "中级":
GameUtils.NUM_MINES = 40;
GameUtils.MAP_W = 16;
GameUtils.MAP_H = 16;
break;
case "高级":
GameUtils.NUM_MINES =99;
GameUtils.MAP_W = 30;
GameUtils.MAP_H = 16;
break;
default:
}
}
}
package experimentFinal1;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
public class MineSweeping {
/*
将一些小组件绘制完成 绘制整体的窗口大小 把雷区放进来
*/
private static int mineNum = 0;
private static boolean isChanged = false; //判断游戏对局是否改变
private static boolean isRestart = false; //判断游戏是否重开
private static boolean isAdd = false;
private static JLabel label1, label2;
private static GamePanel gp;
public MineSweeping() {
if (!isAdd) {
GameUtils.addHero();
isAdd = true;
}
isRestart = false;
isChanged = false;
GameUtils.isClick = false;//判断是否点击
// 绘制窗口
JFrame jFrame = new JFrame("扫雷");
jFrame.setBounds(600,
200,
GameUtils.OFFSET * 2 + GameUtils.MAP_W * GameUtils.DELL_LENGTH,
GameUtils.OFFSET * 2 + GameUtils.MAP_H * GameUtils.DELL_LENGTH);
jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
jFrame.setLocationRelativeTo(null); //让图形界面窗口居中显示
jFrame.setLayout(null);
// 绘制倒计时
label1 = new JLabel("0");
label1.setBounds(10, 0, 120, 20);
jFrame.add(label1);
// 绘制剩余旗子数
label2 = new JLabel("" + mineNum);
label2.setBounds(jFrame.getWidth() - (int)( GameUtils.OFFSET),
0,
100,
20);//设置棋子数字的大小和位置
jFrame.add(label2);
// 绘制重置按钮
JButton restartBt = new JButton(GameUtils.face);
restartBt.setBounds(GameUtils.OFFSET + (GameUtils.MAP_W * GameUtils.DELL_LENGTH / 2) - 15,
0,
30,
30);
restartBt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
jFrame.dispose();
isRestart = true;
GameUtils.isClick = false;
new MineSweeping();
}
});//设置重开的鼠标点击时间
jFrame.add(restartBt);
JButton heroBt = new JButton("英雄榜");
heroBt.setBounds(GameUtils.OFFSET + (GameUtils.MAP_W * GameUtils.DELL_LENGTH / 2) + 20,
0,
50,
20);
heroBt.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
new HeroesDialog();
}
});//设置英雄榜点击事件
jFrame.add(heroBt);
// 难度等级选择绘制
JComboBox comboBox = new JComboBox();
comboBox.addItem("难度选择");
comboBox.addItem("初级");
comboBox.addItem("中级");
comboBox.addItem("高级");
comboBox.setBounds(GameUtils.OFFSET - 10, 0, 80, 20);
jFrame.add(comboBox);
// 绘制雷区
gp = new GamePanel();
gp.setBounds(GameUtils.OFFSET,
GameUtils.OFFSET,
GameUtils.MAP_W * GameUtils.DELL_LENGTH,
GameUtils.MAP_H * GameUtils.DELL_LENGTH);
//绘制雷区的大小和位置
jFrame.add(gp);
jFrame.setVisible(true);
comboBox.addItemListener(new ItemListener() {
//选择难度的按钮事件
@Override
public void itemStateChanged(ItemEvent e) {
if (!isChanged) {
jFrame.dispose();
new LevelSelect((String) comboBox.getSelectedItem());
isRestart = true;
GameUtils.isClick = false;
isChanged = true;
new MineSweeping();
} else {
isChanged = false;
}
}
});
}
// 修改旗子数
public static void setMineNum(int i) {
mineNum = i;
label2.setText("剩余:" + mineNum);
}
// 多线程绘制倒计时
public static class TimeClock extends Thread {
long startTime = System.currentTimeMillis() / 1000;
@Override
public void run() {
while (true) {
long endTime = System.currentTimeMillis() / 1000;
label1.setText("" + (endTime - startTime));
if (isRestart || GameUtils.gameSate == -1 || GameUtils.gameSate == 1) {
if (isRestart) {
isRestart = false;
}
GameUtils.GAME_TIME = (int) (endTime - startTime);
GameUtils.gameSate = 0;
break;
}
}
}
}
}
package experimentFinal1;
import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import java.awt.event.MouseEvent;
public class VictoryDialog extends JDialog {
public VictoryDialog(int time) {
this.setVisible(true);
this.setSize(200, 200);
this.setLocationRelativeTo(null);
JPanel panel = new JPanel();
panel.setLayout(null);
JLabel label1 = new JLabel("扫雷成功!共用时" + time + "s");
JLabel label2 = new JLabel("敢问英雄大名是");
label1.setBounds(40, 0, 150, 30);
label2.setBounds(50, 30, 100, 30);
panel.add(label1);
panel.add(label2);
JTextField textField = new JTextField();
textField.setBounds(0, 60, 200, 30);
panel.add(textField);
JButton addBtn = new JButton("确定");
addBtn.setBounds(60, 120, 70, 20);
panel.add(addBtn);
addBtn.addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
dispose();
String name = textField.getText();
Hero hero = new Hero(name, time);
GameUtils.heroesList.add(hero);
new HeroesDialog();
}
}
});
this.add(panel);
}
}
七、游戏内图片