原文: https://zetcode.com/tutorials/javagamestutorial/minesweeper/

在 Java 2D 游戏教程的这一部分中,我们创建一个扫雷游戏克隆。 源代码和图像可以在作者的 Github Java-Minesweeper-Game 存储库中找到。

扫雷

《扫雷》是一款流行的棋盘游戏,默认情况下附带许多操作系统。 游戏的目标是扫雷场中的所有地雷。 如果玩家单击包含地雷的单元格,则地雷会引爆,游戏结束。

单元格可以包含数字,也可以为空白。 该数字表示与该特定单元相邻的地雷数量。 我们通过右键单击在单元格上设置标记。 通过这种方式,我们表明我们相信,有一个地雷。

Java Minesweeper 游戏的开发

游戏包含两个类别:BoardMinesweepersrc/resources目录中有 13 张图片。

com/zetcode/Board.java

  1. package com.zetcode;
  2. import java.awt.Dimension;
  3. import java.awt.Graphics;
  4. import java.awt.Image;
  5. import java.awt.event.MouseAdapter;
  6. import java.awt.event.MouseEvent;
  7. import java.util.Random;
  8. import javax.swing.ImageIcon;
  9. import javax.swing.JLabel;
  10. import javax.swing.JPanel;
  11. public class Board extends JPanel {
  12. private final int NUM_IMAGES = 13;
  13. private final int CELL_SIZE = 15;
  14. private final int COVER_FOR_CELL = 10;
  15. private final int MARK_FOR_CELL = 10;
  16. private final int EMPTY_CELL = 0;
  17. private final int MINE_CELL = 9;
  18. private final int COVERED_MINE_CELL = MINE_CELL + COVER_FOR_CELL;
  19. private final int MARKED_MINE_CELL = COVERED_MINE_CELL + MARK_FOR_CELL;
  20. private final int DRAW_MINE = 9;
  21. private final int DRAW_COVER = 10;
  22. private final int DRAW_MARK = 11;
  23. private final int DRAW_WRONG_MARK = 12;
  24. private final int N_MINES = 40;
  25. private final int N_ROWS = 16;
  26. private final int N_COLS = 16;
  27. private final int BOARD_WIDTH = N_COLS * CELL_SIZE + 1;
  28. private final int BOARD_HEIGHT = N_ROWS * CELL_SIZE + 1;
  29. private int[] field;
  30. private boolean inGame;
  31. private int minesLeft;
  32. private Image[] img;
  33. private int allCells;
  34. private final JLabel statusbar;
  35. public Board(JLabel statusbar) {
  36. this.statusbar = statusbar;
  37. initBoard();
  38. }
  39. private void initBoard() {
  40. setPreferredSize(new Dimension(BOARD_WIDTH, BOARD_HEIGHT));
  41. img = new Image[NUM_IMAGES];
  42. for (int i = 0; i < NUM_IMAGES; i++) {
  43. var path = "src/resources/" + i + ".png";
  44. img[i] = (new ImageIcon(path)).getImage();
  45. }
  46. addMouseListener(new MinesAdapter());
  47. newGame();
  48. }
  49. private void newGame() {
  50. int cell;
  51. var random = new Random();
  52. inGame = true;
  53. minesLeft = N_MINES;
  54. allCells = N_ROWS * N_COLS;
  55. field = new int[allCells];
  56. for (int i = 0; i < allCells; i++) {
  57. field[i] = COVER_FOR_CELL;
  58. }
  59. statusbar.setText(Integer.toString(minesLeft));
  60. int i = 0;
  61. while (i < N_MINES) {
  62. int position = (int) (allCells * random.nextDouble());
  63. if ((position < allCells)
  64. && (field[position] != COVERED_MINE_CELL)) {
  65. int current_col = position % N_COLS;
  66. field[position] = COVERED_MINE_CELL;
  67. i++;
  68. if (current_col > 0) {
  69. cell = position - 1 - N_COLS;
  70. if (cell >= 0) {
  71. if (field[cell] != COVERED_MINE_CELL) {
  72. field[cell] += 1;
  73. }
  74. }
  75. cell = position - 1;
  76. if (cell >= 0) {
  77. if (field[cell] != COVERED_MINE_CELL) {
  78. field[cell] += 1;
  79. }
  80. }
  81. cell = position + N_COLS - 1;
  82. if (cell < allCells) {
  83. if (field[cell] != COVERED_MINE_CELL) {
  84. field[cell] += 1;
  85. }
  86. }
  87. }
  88. cell = position - N_COLS;
  89. if (cell >= 0) {
  90. if (field[cell] != COVERED_MINE_CELL) {
  91. field[cell] += 1;
  92. }
  93. }
  94. cell = position + N_COLS;
  95. if (cell < allCells) {
  96. if (field[cell] != COVERED_MINE_CELL) {
  97. field[cell] += 1;
  98. }
  99. }
  100. if (current_col < (N_COLS - 1)) {
  101. cell = position - N_COLS + 1;
  102. if (cell >= 0) {
  103. if (field[cell] != COVERED_MINE_CELL) {
  104. field[cell] += 1;
  105. }
  106. }
  107. cell = position + N_COLS + 1;
  108. if (cell < allCells) {
  109. if (field[cell] != COVERED_MINE_CELL) {
  110. field[cell] += 1;
  111. }
  112. }
  113. cell = position + 1;
  114. if (cell < allCells) {
  115. if (field[cell] != COVERED_MINE_CELL) {
  116. field[cell] += 1;
  117. }
  118. }
  119. }
  120. }
  121. }
  122. }
  123. private void find_empty_cells(int j) {
  124. int current_col = j % N_COLS;
  125. int cell;
  126. if (current_col > 0) {
  127. cell = j - N_COLS - 1;
  128. if (cell >= 0) {
  129. if (field[cell] > MINE_CELL) {
  130. field[cell] -= COVER_FOR_CELL;
  131. if (field[cell] == EMPTY_CELL) {
  132. find_empty_cells(cell);
  133. }
  134. }
  135. }
  136. cell = j - 1;
  137. if (cell >= 0) {
  138. if (field[cell] > MINE_CELL) {
  139. field[cell] -= COVER_FOR_CELL;
  140. if (field[cell] == EMPTY_CELL) {
  141. find_empty_cells(cell);
  142. }
  143. }
  144. }
  145. cell = j + N_COLS - 1;
  146. if (cell < allCells) {
  147. if (field[cell] > MINE_CELL) {
  148. field[cell] -= COVER_FOR_CELL;
  149. if (field[cell] == EMPTY_CELL) {
  150. find_empty_cells(cell);
  151. }
  152. }
  153. }
  154. }
  155. cell = j - N_COLS;
  156. if (cell >= 0) {
  157. if (field[cell] > MINE_CELL) {
  158. field[cell] -= COVER_FOR_CELL;
  159. if (field[cell] == EMPTY_CELL) {
  160. find_empty_cells(cell);
  161. }
  162. }
  163. }
  164. cell = j + N_COLS;
  165. if (cell < allCells) {
  166. if (field[cell] > MINE_CELL) {
  167. field[cell] -= COVER_FOR_CELL;
  168. if (field[cell] == EMPTY_CELL) {
  169. find_empty_cells(cell);
  170. }
  171. }
  172. }
  173. if (current_col < (N_COLS - 1)) {
  174. cell = j - N_COLS + 1;
  175. if (cell >= 0) {
  176. if (field[cell] > MINE_CELL) {
  177. field[cell] -= COVER_FOR_CELL;
  178. if (field[cell] == EMPTY_CELL) {
  179. find_empty_cells(cell);
  180. }
  181. }
  182. }
  183. cell = j + N_COLS + 1;
  184. if (cell < allCells) {
  185. if (field[cell] > MINE_CELL) {
  186. field[cell] -= COVER_FOR_CELL;
  187. if (field[cell] == EMPTY_CELL) {
  188. find_empty_cells(cell);
  189. }
  190. }
  191. }
  192. cell = j + 1;
  193. if (cell < allCells) {
  194. if (field[cell] > MINE_CELL) {
  195. field[cell] -= COVER_FOR_CELL;
  196. if (field[cell] == EMPTY_CELL) {
  197. find_empty_cells(cell);
  198. }
  199. }
  200. }
  201. }
  202. }
  203. @Override
  204. public void paintComponent(Graphics g) {
  205. int uncover = 0;
  206. for (int i = 0; i < N_ROWS; i++) {
  207. for (int j = 0; j < N_COLS; j++) {
  208. int cell = field[(i * N_COLS) + j];
  209. if (inGame && cell == MINE_CELL) {
  210. inGame = false;
  211. }
  212. if (!inGame) {
  213. if (cell == COVERED_MINE_CELL) {
  214. cell = DRAW_MINE;
  215. } else if (cell == MARKED_MINE_CELL) {
  216. cell = DRAW_MARK;
  217. } else if (cell > COVERED_MINE_CELL) {
  218. cell = DRAW_WRONG_MARK;
  219. } else if (cell > MINE_CELL) {
  220. cell = DRAW_COVER;
  221. }
  222. } else {
  223. if (cell > COVERED_MINE_CELL) {
  224. cell = DRAW_MARK;
  225. } else if (cell > MINE_CELL) {
  226. cell = DRAW_COVER;
  227. uncover++;
  228. }
  229. }
  230. g.drawImage(img[cell], (j * CELL_SIZE),
  231. (i * CELL_SIZE), this);
  232. }
  233. }
  234. if (uncover == 0 && inGame) {
  235. inGame = false;
  236. statusbar.setText("Game won");
  237. } else if (!inGame) {
  238. statusbar.setText("Game lost");
  239. }
  240. }
  241. private class MinesAdapter extends MouseAdapter {
  242. @Override
  243. public void mousePressed(MouseEvent e) {
  244. int x = e.getX();
  245. int y = e.getY();
  246. int cCol = x / CELL_SIZE;
  247. int cRow = y / CELL_SIZE;
  248. boolean doRepaint = false;
  249. if (!inGame) {
  250. newGame();
  251. repaint();
  252. }
  253. if ((x < N_COLS * CELL_SIZE) && (y < N_ROWS * CELL_SIZE)) {
  254. if (e.getButton() == MouseEvent.BUTTON3) {
  255. if (field[(cRow * N_COLS) + cCol] > MINE_CELL) {
  256. doRepaint = true;
  257. if (field[(cRow * N_COLS) + cCol] <= COVERED_MINE_CELL) {
  258. if (minesLeft > 0) {
  259. field[(cRow * N_COLS) + cCol] += MARK_FOR_CELL;
  260. minesLeft--;
  261. String msg = Integer.toString(minesLeft);
  262. statusbar.setText(msg);
  263. } else {
  264. statusbar.setText("No marks left");
  265. }
  266. } else {
  267. field[(cRow * N_COLS) + cCol] -= MARK_FOR_CELL;
  268. minesLeft++;
  269. String msg = Integer.toString(minesLeft);
  270. statusbar.setText(msg);
  271. }
  272. }
  273. } else {
  274. if (field[(cRow * N_COLS) + cCol] > COVERED_MINE_CELL) {
  275. return;
  276. }
  277. if ((field[(cRow * N_COLS) + cCol] > MINE_CELL)
  278. && (field[(cRow * N_COLS) + cCol] < MARKED_MINE_CELL)) {
  279. field[(cRow * N_COLS) + cCol] -= COVER_FOR_CELL;
  280. doRepaint = true;
  281. if (field[(cRow * N_COLS) + cCol] == MINE_CELL) {
  282. inGame = false;
  283. }
  284. if (field[(cRow * N_COLS) + cCol] == EMPTY_CELL) {
  285. find_empty_cells((cRow * N_COLS) + cCol);
  286. }
  287. }
  288. }
  289. if (doRepaint) {
  290. repaint();
  291. }
  292. }
  293. }
  294. }
  295. }

首先,我们定义游戏中使用的常量。

  1. private final int NUM_IMAGES = 13;
  2. private final int CELL_SIZE = 15;

此游戏中使用了十三张图像。 一个牢房最多可被八个地雷包围,因此我们需要一号到八号。 我们需要一个空单元格,一个地雷,一个被遮盖的单元格,一个标记的单元格,最后需要一个错误标记的单元格的图像。 每个图像的大小为15x15像素。

  1. private final int COVER_FOR_CELL = 10;
  2. private final int MARK_FOR_CELL = 10;
  3. private final int EMPTY_CELL = 0;
  4. ...

地雷区是数字数组。 例如,0 表示一个空单元格。 数字 10 用于电池盖和标记。 使用常量可以提高代码的可读性。

  1. private final int MINE_CELL = 9;

MINE_CELL表示包含地雷的单元。

  1. private final int COVERED_MINE_CELL = MINE_CELL + COVER_FOR_CELL;
  2. private final int MARKED_MINE_CELL = COVERED_MINE_CELL + MARK_FOR_CELL;

COVERED_MINE_CELL用于覆盖并包含地雷的区域。 MARKED_MINE_CELL代码是由用户标记的隐蔽地雷单元。

  1. private final int DRAW_MINE = 9;
  2. private final int DRAW_COVER = 10;
  3. private final int DRAW_MARK = 11;
  4. private final int DRAW_WRONG_MARK = 12;

这些竞争因素决定是否绘制地雷,地雷覆盖物,标记和标记错误的单元。

  1. private final int N_MINES = 40;
  2. private final int N_ROWS = 16;
  3. private final int N_COLS = 16;

我们游戏中的雷区有 40 个隐藏的地雷。 该字段中有 16 行和 16 列。 因此,雷场中共有 262 个牢房。

  1. private int[] field;

该字段是数字数组。 字段中的每个单元格都有一个特定的编号。 例如,一个矿井的编号为 9。一个矿井的编号为 2 意味着它与两个矿井相邻。 数字已添加。 例如,一个被覆盖的地雷的编号为 19,地雷的编号为 9,电池盖的编号为 10,依此类推。

  1. private boolean inGame;

inGame变量确定我们是在游戏中还是游戏结束。

  1. private int minesLeft;

minesLeft变量要标记为左侧的地雷数量。

  1. for (int i = 0; i < NUM_IMAGES; i++) {
  2. var path = "src/resources/" + i + ".png";
  3. img[i] = (new ImageIcon(path)).getImage();
  4. }

我们将图像加载到图像数组中。 这些图像分别命名为0.png1.png12.png

newGame()启动扫雷游戏。

  1. allCells = N_ROWS * N_COLS;
  2. field = new int[allCells];
  3. for (int i = 0; i < allCells; i++) {
  4. field[i] = COVER_FOR_CELL;
  5. }

这些线设置了雷区。 默认情况下覆盖每个单元格。

  1. int i = 0;
  2. while (i < N_MINES) {
  3. int position = (int) (allCells * random.nextDouble());
  4. if ((position < allCells)
  5. && (field[position] != COVERED_MINE_CELL)) {
  6. int current_col = position % N_COLS;
  7. field[position] = COVERED_MINE_CELL;
  8. i++;
  9. ...

在白色周期中,我们将所有地雷随机放置在野外。

  1. cell = position - N_COLS;
  2. if (cell >= 0) {
  3. if (field[cell] != COVERED_MINE_CELL) {
  4. field[cell] += 1;
  5. }
  6. }

每个单元最多可以包围八个单元。 (这不适用于边界单元。)我们为每个随机放置的地雷增加相邻单元的数量。 在我们的示例中,我们向相关单元格的顶部邻居添加 1。

find_empty_cells()方法中,我们找到了空单元格。 如果玩家单击雷区,则游戏结束。 如果他单击与地雷相邻的单元,则会发现一个数字,该数字指示该单元与地雷相邻的数量。 单击一个空单元格会导致发现许多其他空单元格以及带有数字的单元格,这些数字在空边界的空间周围形成边界。 我们使用递归算法来查找空单元格。

  1. cell = j - 1;
  2. if (cell <= 0) {
  3. if (field[cell] > MINE_CELL) {
  4. field[cell] -= COVER_FOR_CELL;
  5. if (field[cell] == EMPTY_CELL) {
  6. find_empty_cells(cell);
  7. }
  8. }
  9. }

在此代码中,我们检查位于相关空单元格左侧的单元格。 如果不为空,则将其覆盖。 如果为空,则通过递归调用find_empty_cells()方法来重复整个过程。

paintComponent()方法将数字转换为图像。

  1. if (!inGame) {
  2. if (cell == COVERED_MINE_CELL) {
  3. cell = DRAW_MINE;
  4. } else if (cell == MARKED_MINE_CELL) {
  5. cell = DRAW_MARK;
  6. } else if (cell > COVERED_MINE_CELL) {
  7. cell = DRAW_WRONG_MARK;
  8. } else if (cell > MINE_CELL) {
  9. cell = DRAW_COVER;
  10. }
  11. } ...

如果游戏结束并且我们输了,我们将显示所有未发现的地雷(如果有的话),并显示所有错误标记的单元格(如果有)。

  1. g.drawImage(img[cell], (j * CELL_SIZE),
  2. (i * CELL_SIZE), this);

此代码行绘制了窗口上的每个单元格。

  1. if (uncover == 0 && inGame) {
  2. inGame = false;
  3. statusbar.setText("Game won");
  4. } else if (!inGame) {
  5. statusbar.setText("Game lost");
  6. }

如果没有什么可以发现的,我们就赢了。 如果inGame变量设置为false,我们将丢失。

mousePressed()方法中,我们对鼠标单击做出反应。 扫雷游戏完全由鼠标控制。 我们对鼠标左键和右键单击做出反应。

  1. int x = e.getX();
  2. int y = e.getY();

我们确定鼠标指针的 x 和 y 坐标。

  1. int cCol = x / CELL_SIZE;
  2. int cRow = y / CELL_SIZE;

我们计算雷区的相应列和行。

  1. if ((x < N_COLS * CELL_SIZE) && (y < N_ROWS * CELL_SIZE)) {

我们检查我们是否位于雷区。

  1. if (e.getButton() == MouseEvent.BUTTON3) {

地雷的发现是通过鼠标右键完成的。

  1. field[(cRow * N_COLS) + cCol] += MARK_FOR_CELL;
  2. minesLeft--;

如果右键单击未标记的单元格,则将MARK_FOR_CELL添加到表示该单元格的数字中。 这导致在paintComponent()方法中绘制带有标记的覆盖单元。

  1. field[(cRow * N_COLS) + cCol] -= MARK_FOR_CELL;
  2. minesLeft++;
  3. var msg = Integer.toString(minesLeft);
  4. statusbar.setText(msg);

如果我们在已经标记的单元格上单击鼠标左键,我们将删除标记并增加要标记的单元格的数量。

  1. if (field[(cRow * N_COLS) + cCol] > COVERED_MINE_CELL) {
  2. return;
  3. }

如果单击覆盖并标记的单元格,则不会发生任何事情。 必须首先通过另一次右键单击来发现它,然后才可以在其上单击鼠标左键。

  1. field[(cRow * N_COLS) + cCol] -= COVER_FOR_CELL;

左键单击可从单元中移除盖子。

  1. if (field[(cRow * N_COLS) + cCol] == MINE_CELL) {
  2. inGame = false;
  3. }
  4. if (field[(cRow * N_COLS) + cCol] == EMPTY_CELL) {
  5. find_empty_cells((cRow * N_COLS) + cCol);
  6. }

万一我们左键单击地雷,游戏就结束了。 如果我们在空白单元格上单击鼠标左键,我们将调用find_empty_cells()方法,该方法递归地找到所有相邻的空白单元格。

  1. if (doRepaint) {
  2. repaint();
  3. }

如果需要重新粉刷电路板(例如,设置或移除了标记),我们将调用repaint()方法。

com/zetcode/Minesweeper.java

  1. package com.zetcode;
  2. import java.awt.BorderLayout;
  3. import java.awt.EventQueue;
  4. import javax.swing.JFrame;
  5. import javax.swing.JLabel;
  6. /**
  7. * Java Minesweeper Game
  8. *
  9. * Author: Jan Bodnar
  10. * Website: http://zetcode.com
  11. */
  12. public class Minesweeper extends JFrame {
  13. private JLabel statusbar;
  14. public Minesweeper() {
  15. initUI();
  16. }
  17. private void initUI() {
  18. statusbar = new JLabel("");
  19. add(statusbar, BorderLayout.SOUTH);
  20. add(new Board(statusbar));
  21. setResizable(false);
  22. pack();
  23. setTitle("Minesweeper");
  24. setLocationRelativeTo(null);
  25. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  26. }
  27. public static void main(String[] args) {
  28. EventQueue.invokeLater(() -> {
  29. var ex = new Minesweeper();
  30. ex.setVisible(true);
  31. });
  32. }
  33. }

这是主要的类。

  1. setResizable(false);

窗口大小固定。 为此,我们使用setResizable()方法。

Java 扫雷 - 图1

图:扫雷

在 Java 2D 游戏教程的这一部分中,我们创建了扫雷游戏的 Java 复制版本。