4

无聊的周末用Java写个扫雷小游戏

 2 years ago
source link: https://blog.51cto.com/u_13604316/4907388
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

周末无聊,用 ​​Java 写了一个扫雷程序,说起来,这个应该是在学校的时候,写会比较好玩,毕竟自己实现一个小游戏,还是比较好玩的。说实话,扫雷程序里面核心的东西,只有点击的时候,去触发更新数据这一步。​

Swing 是过时了,但是好玩不会过时,不喜勿喷无聊的周末用Java写个扫雷小游戏_广度优先搜索

源码的地址:https://github.com/Damaer/Game/tree/main/SweepMine

下面讲讲里面的设计:

  • 数据结构设计
  • 视图和数据尽可能分开
  • 点击时候使用​​BFS​​扫描
  • 判断成功失败

数据结构设计

在这个程序里面,为了方便,使用了全局的数据类​​Data​​类来维护整个游戏的数据,直接设置为静态变量,也就是一次只能有一个游戏窗口运行,否则会有数据安全问题。(仅仅是为了方便)

有以下的数据(部分代码):

public class Data {
// 游戏状态
public static Status status = Status.LOADING;
// 雷区大小
public static int size = 16;
// 雷的数量
public static int numOfMine = 0;
// 表示是否有雷,1:有,0没有
public static int[][] maps = null;
// 是否被访问
public static boolean[][] visited = null;
// 周边雷的数量
public static int[][] nums = null;
// 是否被标记
public static boolean[][] flags = null;
// 上次被访问的块坐标
public static Point lastVisitedPoint = null;
// 困难模式
private static DifficultModeEnum mode;
...
}

需要维护的数据如下:

  • 游戏状态:是否开始,结束,成功,失败等等
  • 模式:简单,中等或者困难,这个会影响自动生成的雷的数量
  • 雷区的大小:16*16的小方块
  • 雷的数量:与模式选择有关,是个随机数
  • 标识每个方块是否有雷:最基础的数据,生成之后需要同步更新这个数据
  • 标识每个方块是否被扫过:默认没有扫过
  • 每个方块周边类雷的数量:生成的时候同步计算该结果,不想每次点击后再计算,毕竟是个不会更新的数据,一劳永逸
  • 标识方块是否被标记:扫雷的时候我们使用小旗子标记方块,表示这里是雷,标识完所有的雷的时候,成功
  • 上次访问的方块坐标:这个其实可以不记录,但是为了表示爆炸效果,与其他的雷展示不一样,故而记录下来

视图与数据分开

尽量遵循一个原则,视图与数据或者数据变更分开,方便维护。我们知道​​Java​​里面是用​​Swing​​来画图形界面,这个东西确实难画,视图写得比较复杂但是画不出什么东西。

无聊的周末用Java写个扫雷小游戏_广度优先搜索_02

视图与数据分开,也是几乎所有框架的优秀特点,主要是方便维护,如果视图和数据糅合在一起,更新数据,还要操作视图,那就会比较乱。(当然我写的是粗糙版本,只是简单区分了一下)

在这个扫雷程序里面基本都是点击事件,触发了数据变更,数据变更后,调用视图刷新,视图渲染的逻辑与数据变更的逻辑分开维护。

每个小方块都添加了点击事件,​​Data.visit(x, y)​​是数据刷新,​​repaintBlocks()​​是刷新视图,具体的代码就不放了,有兴趣可以​​Github​​看看源代码:

new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
if (Data.status == Status.GOING) {
int c = e.getButton(); // 得到按下的鼠标键
Block block = (Block) e.getComponent();
int x = block.getPoint_x();
int y = block.getPoint_y();
if (c == MouseEvent.BUTTON1) {
Data.visit(x, y);
} else if (c == MouseEvent.BUTTON3) {// 推断是鼠标右键按下
if (!Data.visited[x][y]) {
Data.flags[x][y] = !Data.flags[x][y];
}
}
}
repaintBlocks();
}
}

这里很遗憾的一点是每个方块里面还有一个背景的`​​url​​没有抽取出来,这个是变化的数据,不应该放在视图里面:

public class Block extends JPanel {
private int point_x;
private int point_y;

private String backgroundPath = ImgPath.DEFAULT;

public Block(int x, int y) {
this.point_x = x;
this.point_y = y;
setBorder(BorderFactory.createEtchedBorder());
}
}

重新设置方块背景,需要居中处理,重新绘制,重写​​void paintComponent(Graphics g)​​方法即可:

@Override
protected void paintComponent(Graphics g) {
refreshBackground();
URL url = getClass().getClassLoader().getResource(backgroundPath);
ImageIcon icon = new ImageIcon(url);
if (backgroundPath.equals(ImgPath.DEFAULT) || backgroundPath.equals(ImgPath.FLAG)
|| backgroundPath.equals(String.format(ImgPath.NUM, 0))) {
g.drawImage(icon.getImage(), 0, 0, getWidth(), getHeight(), this);
} else {
int x = (int) (getWidth() * 0.1);
int y = (int) (getHeight() * 0.15);
g.drawImage(icon.getImage(), x, y, getWidth() - 2 * x, getHeight() - 2 * y, this);
}
}

BFS扫描

​BFS​​,也称为广度优先搜索,这算是扫雷里面的核心知识点,也就是点击的时候,如果当前方块是空的,那么就会触发扫描周边的方块,同时周边方块如果也是空的,会继续递归下去,我用了广度优先搜索,也就是先将它们放到队列里面,取出来,再判断是否为空,再将周边符合的方块添加进去,进行一一处理。

广度优先搜索在这里不展开,其本质是优先搜索与其直接关联的数据,也就是方块周围的点,这也是为什么需要队列的原因,我们需要队列来保存遍历的顺序。

public static void visit(int x, int y) {
lastVisitedPoint.x = x;
lastVisitedPoint.y = y;
if (maps[x][y] == 1) {
status = Status.FAILED;
// 游戏结束,暴露所有的雷
} else {
// 点击的不是雷
Queue<Point> points = new LinkedList<>();
points.add(new Point(x, y));
while (!points.isEmpty()) {
Point point = points.poll();
visited[point.x][point.y] = true;
if (nums[point.x][point.y] == 0) {
addToVisited(points, point.x, point.y);
}
}
}
}

public static void addToVisited(Queue<Point> points, int i, int j) {
int x = i - 1;
while (x <= i + 1) {
if (x >= 0 && x < size) {
int y = j - 1;
while (y <= j + 1) {
if (y >= 0 && y < size) {
if (!(x == i && j == y)) {
// 没访问过且不是雷
if (!visited[x][y] && maps[x][y] == 0) {
points.add(new Point(x, y));
}
}
}
y++;
}
}
x++;
}
}

值得注意的是,周边的点,如果它的周边没有雷,那么会继续拓展,但是只要周边有雷,就会停止拓展,只会显示数字。

无聊的周末用Java写个扫雷小游戏_数据_03

判断成功失败

当挖到雷的时候,就失败了,同时会将所有的雷暴露出来,为了展示我们当前挖到的点,有爆炸效果,我们记录了上一步操作的点,在刷新视图后,弹窗提示:

无聊的周末用Java写个扫雷小游戏_数据_04

判断成功则需要将所有的雷遍历一次,判断是否被标记出来,这是我简单想的规则,忘记了扫雷是不是这样了,或者可以实现将其他所有非雷区都挖空的时候,成功,也是可以的。

扫雷,一个简单的游戏,无聊的时候可以尝试一下,但是​​Java​​ 的​​Swing​​真的难用,想找一个数据驱动视图修改的框架,但是貌似没有,那就简单实现一下。其实大部分时间都在找图标,测试​​UI​​,核心的代码并没有多少。在这里推荐一下​

​icon​​网站:​​https://www.iconfont.cn/​​,即使是没有什么技术含量的扫雷,写一下还是挺有趣的。

【作者简介】: 秦怀,公众号【

秦怀杂货店】作者,个人网站:http://aphysia.cn,技术之路不在一时,山高水长,纵使缓慢,驰而不息。​

 ​剑指Offer全部题解PDF​​​

 ​开源编程笔记​


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK