I was bored on weekends, so I used Java
write a mine-sweeping program. Speaking of which, it should be more fun to write when I was in school. After all, it is more fun to implement a small game by myself. To be honest, the core thing in the minesweeper program is to trigger the step of updating the data only when it is clicked.
Swing is outdated, but fun will not be outdated, don't spray
Source code address: https://github.com/Damaer/Game/tree/main/SweepMine
Here's the design inside:
- data structure design
- View and data as separate as possible
- Scan with
BFS
- Judging success or failure
data structure design
In this program, for convenience, the global data class Data
is used to maintain the data of the entire game, which is directly set as a static variable, that is, only one game window can run at a time, otherwise there will be data security problems. (just for convenience)
There is the following data (part of the code):
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;
...
}
The data to be maintained is as follows:
- Game state: whether it started, ended, succeeded, failed, etc.
- Mode: Easy, Medium or Hard, this affects the number of automatically generated mines
- The size of the minefield: 16*16 small squares
- The number of mines: related to mode selection, it is a random number
- Identify whether each block has thunder: the most basic data, which needs to be updated synchronously after generation
- Identifies whether each square has been swept: the default is not swept
- The number of mines around each block: The result is calculated synchronously when it is generated. I don’t want to calculate it every time I click it. After all, it is a data that will not be updated, once and for all
- Whether the marking block is marked: When clearing the mine, we use a small flag to mark the square, indicating that this is a mine. When all mines are marked, it is successful.
- The coordinates of the last visited block: This can actually not be recorded, but in order to represent the explosion effect, it is different from other mine displays, so it is recorded.
View separate from data
Try to follow a principle that the view is separated from the data or data changes, which is convenient for maintenance. We know that Java
uses Swing
to draw the graphical interface. This thing is really difficult to draw. The view is more complicated, but nothing can be drawn.
The separation of views and data is also an excellent feature of almost all frameworks, mainly for ease of maintenance. If views and data are combined, update data, and manipulate views, it will be messy. (Of course I wrote a rough version, just a brief distinction)
In this minesweeper program, there are basically click events that trigger data changes. After the data changes, the view refresh is called, and the logic of view rendering is maintained separately from the logic of data changes.
A click event is added to each small square, Data.visit(x, y)
is the data refresh, repaintBlocks()
is the refresh view, the specific code will not be put, if you are interested, you can look at the source code at 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();
}
}
It is a pity here that there is also a background `url
in each square that has not been extracted. This is changed data and should not be placed in the view:
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());
}
}
To reset the square background, you need to center, redraw, and rewrite the void paintComponent(Graphics g)
method:
@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 scan
BFS
, also known as breadth-first search, this is the core knowledge point in mine sweeping, that is, when you click, if the current square is empty, it will trigger the scan of the surrounding squares, and if the surrounding squares are also empty, it will continue Going down recursively, I used breadth-first search, that is, put them in the queue first, take them out, and then judge whether they are empty, and then add the matching blocks around them to process them one by one.
The breadth-first search is not expanded here. Its essence is to first search for the data directly associated with it, that is, the points around the square. This is why a queue is needed. We need a queue to save the order of traversal.
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++;
}
}
It is worth noting that the surrounding points will continue to expand if there are no mines around them, but as long as there are mines around, they will stop expanding and only display numbers.
Judging success or failure
When digging mines, it fails, and all mines will be exposed at the same time. In order to show the points we have dug, there is an explosion effect, we record the points of the previous operation. After refreshing the view, a pop-up window prompts :
To judge success, you need to traverse all the mines once to determine whether they are marked. This is the rule I simply think. I forgot whether this is the case for mine sweeping, or when all other non-minefields can be hollowed out, it is successful. it is also fine.
Summarize
Minesweeper, a simple game, you can try it when you are bored, but Java
of Swing
really difficult to use. I want to find a data-driven view modification framework, but it seems that there is no one, so I will simply implement it. In fact, most of the time is looking for icons, testing UI
, the core code is not much.
Here I recommend the icon
website: https://www.iconfont.cn/
, even if it is a mine sweeper without any technical content, it is quite interesting to write about it.
【About the author】 :
Qin Huai, [161e4bf9b224ca Qinhuai Grocery Store ], personal website: http://aphysia.cn
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。