Java语言实现扫雷的设计与源码(Swing的学习总结)

xiaoxiao2021-02-28  77

扫雷,是一款耐玩有意思的小游戏。但是通过代码实现对于新手来说并不算简单。本人是一个新手,通过这几天摸索、分析设计,也算基本能做出来了,当然有部分功能尚未实现,如果有人能指出错误,那就更好了。下面来分享一下学习的历程。博文有点长,如果只想要源码的,可以直接拉到最下面。 效果图:

扫雷核心算法 ps:本人算法跟数据结构涉及不算多,所以有些算法虽然能实现功能,但是对于系统内存占用会比较大。如果有人对于扫雷算法有更好的实现方法,欢迎指出。

1.随机生成雷: 雷的表示:可以用二维数组的下标表示,所以可以用随机数Math.random()方法来生成随机横纵坐标,random()方法是随机生成0~1之间的小数,但是不会生成0和1。所以random()*16就表示随机生成0~16之间的浮点数,不包括0和16。 因为坐标是整数类型,所以生成的随机横纵坐标需要强制转换为int类型 有一点需要注意的是,强制转换类型并不是四舍五入转换,例如: random()*30随机生成一个数的值为29.99999999999,转换为会变为29。

/** * @param mineNum:表示要生成雷的总数 * @param row:表示随机雷的横坐标的int类型 * @param col:表示随机雷的纵坐标的int类型 * @param EMPTY:值为0,表示该组件尚未设置任何东西 * @param MINE:值为1,表示该组件已经设置为雷 */ private void addMine() { for (int mineNum = 1; mineNum <= MINE_NUMBER;) { int row = (int) (Math.random() * 16); int col = (int) (Math.random() * 30); // 为了避免有相同位置的雷的生成,需要简单判断一下 if (map[row][col] == EMPTY) { map[row][col] = MINE; mineLab[row][col].setIcon(img(9));//设置雷的图标 mineNum++; } } }

2.统计每个按钮周围8个格子的雷的总数: 原理如同下图:(i,j)就是我们要统计的点,周围8个格子的坐标就是下图,通过图片我们可以发现,每一行的都是横坐标累加,纵坐标不变。每一列都是横坐标不变,纵坐标累加。所以就可以用两层for循环实现历遍周围八个格的功能了。有一点需要注意的是,如果(i,j)这个点位于边界,那么就有可能发生越界,比如(i,j)是(0,0),那么i-1=-1,数组就会报错,因为数组下标不能为负数,或者大于长度。所以,每次循环都要判断一下 if (i >= 0 && j >= 0 && i < 16 && j < 30) 是否为真

/** * 计算每个格周围八个格的雷的总数量的方法,并返回int类型的雷的数量值 * @param i:传入按钮的横坐标 * @param j:传入按钮的纵坐标 */ private int mN(int i, int j) { int count = 0; // 雷的数量 int row = i; //保存固定的横坐标 int col = j; //保存固定的纵坐标 for (int m = -1; m < 2; m++) { i = row; //使得每次循环的i的初始坐标不变 i += m; for (int n = -1; n < 2; n++) { j = col; //同上 j += n; if (i >= 0 && j >= 0 && i < 16 && j < 30) { if (map[i][j] == MINE) { count++; } } } } return count; }

3.空雷区自动翻开:

private void isBlank(int i, int j) { if (mN(i, j) == 0) { map[i][j] = CHECKED; int row = i; int col = j; for (int m = -1; m < 2; m++) { i = row; i += m; for (int n = -1; n < 2; n++) { j = col; j += n; if (i >= 0 && j >= 0 && i < 16 && j < 30) { if (mN(i, j) == 0) { if (map[i][j] != MINE && map[i][j] != CHECKED) { showLab(i, j); isBlank(i, j); } } else { showLab(i, j); } } } } } }

4.统计剩余雷数

/** * 判断剩余雷数 */ private void isClean() { int mineFound = MINE_NUMBER_H; for (int i = 0; i < ROW; i++) { for (int j = 0; j < COL; j++) { if (mineFalg[i][j] == IS_FLAG) { mineFound--; } } } mineText.setText("" + mineFound); if (mineFound == 0) { flag = true; showMine(); } }

完整代码:

package view; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Font; import java.awt.GridLayout; import java.awt.Image; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.UIManager; @SuppressWarnings("serial") public class Mine extends JFrame { // 初始化窗口的长、宽的常量 private static final int DEFAULT_WIDTH = 1440; private static final int DEFAULT_HEIGHT = 900; // 字体设置变量 private Font infoNumber = new Font("Arial", Font.PLAIN, 30); // 时间数字字体 private Font fText = new Font("幼圆", Font.BOLD, 20); // 文本字体 private Font fMenu = new Font("微软雅黑", Font.PLAIN, 15); // 文本字体 // 颜色设置变量 private String c_info = "#CDBA96"; // 信息面板背景颜色 private String c_minePan = "#FFFAFA"; // 雷面板背景颜色 // 观感变量 String lafWin = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"; String lafNim = "javax.swing.plaf.nimbus.NimbusLookAndFeel"; // 图标变量 private ImageIcon image; // 判断run()方法是否运行的标志 private boolean flag; // 常量定义 private final int EMPTY = 0; // 雷面板空状态 private final int MINE = 1; // 雷面板雷状态 private final int CHECKED = 2; // 雷面板已检查的状态 private final int NO_FLAG = 0; // 按钮没有设置旗帜的状态 private final int IS_FLAG = 1; // 按钮没有设置旗帜的状态 private final int MINE_NUMBER_H = 60; // 雷的总数量 private final int ROW = 16; // 行数 private final int COL = 30; // 列数 private JTextField timeText; // 显示时间的文本框 private JTextField mineText; // 显示剩余雷数的文本框 private JPanel minePanel; // 主面板,用于放置雷 private JPanel infoPanel; // 信息面板 private JMenuBar menuBar; // 菜单栏 private JPanel[][] mineJan; // 雷面板数组 private JButton[][] mineBut; // 雷按钮数组 private JLabel[][] mineLab; // 雷标签数组 private int[][] map; // 雷面板标志数组 private int[][] mineFalg; // 雷按钮标志数组 ActionListener menuAc = new MenuAction(); // 菜单事件处理类实例 ActionListener buttonAc = new ButtonAction(); // 按钮事件处理类实例 MouseListener mouseAC = new mouseAction(); // 鼠标事件类实例 /** * 程序入口main方法 * @throws Exception */ public static void main(String[] args) throws Exception { Mine frame = new Mine(); // 窗口居中显示 Toolkit kit = Toolkit.getDefaultToolkit(); int screenWidth = kit.getScreenSize().width; int screenHeight = kit.getScreenSize().height; frame.setBounds((screenWidth - DEFAULT_WIDTH) / 2, (screenHeight - DEFAULT_HEIGHT) / 2, DEFAULT_WIDTH, DEFAULT_HEIGHT); frame.setTitle("扫雷"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); frame.setIconImage(new ImageIcon("mine.png").getImage()); // 设置窗口图标 frame.run(); } /** * 初始化方法 */ private void init() throws Exception { flag = false; mineBut = new JButton[ROW][COL]; mineLab = new JLabel[ROW][COL]; for (int i = 0; i < ROW; i++) { for (int j = 0; j < COL; j++) { map = new int[16][30]; map[i][j] = EMPTY; mineFalg = new int[16][30]; mineFalg[i][j] = NO_FLAG; } } for (int i = 0; i < ROW; i++) { for (int j = 0; j < COL; j++) { addMineBut(i, j); addMineLab(i, j); mineJan[i][j].repaint(); } } addMine(); setIcon(); } /** * 实例化窗口的构造方法 */ public Mine() throws Exception { menu(); infoPanel(); minePanel(); } /** * 菜单栏设计 * 一级菜单:游戏(G) 帮助(H) * 二级菜单:新游戏(N) 退出(E) 关于游戏(A) */ // 添加一级菜单的方法 private void addMenu(JMenu menu) { menu.setFont(fMenu); menuBar.add(menu); } // 添加二级菜单的方法 private void addMenuItem(JMenu menu, JMenuItem menuItem) { menuItem.setFont(fMenu); menuItem.addActionListener(menuAc); // 为菜单项注册事件监听器 menu.add(menuItem); } private void menu() throws Exception { menuBar = new JMenuBar(); // 改变菜单栏的观感为window观感 UIManager.setLookAndFeel(lafWin); SwingUtilities.updateComponentTreeUI(menuBar); pack(); JMenu menuG = new JMenu("游戏(G)"); JMenu menuH = new JMenu("帮助(H)"); JMenuItem itemNew = new JMenuItem("新游戏(N)"); JMenuItem itemExit = new JMenuItem("退出(E)"); JMenuItem itemAbout = new JMenuItem("关于扫雷(A)"); addMenu(menuG); addMenu(menuH); addMenuItem(menuG, itemNew); addMenuItem(menuG, itemExit); addMenuItem(menuH, itemAbout); // 设置快捷键 menuG.setMnemonic('G'); // 设置快捷键,Alt+G可以使用,下同 menuH.setMnemonic('H'); itemNew.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_MASK)); // Ctrl+N,下同 itemExit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, InputEvent.CTRL_MASK)); itemAbout.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_MASK)); setJMenuBar(menuBar); } /** * 信息面板设计: * 1.采用默认流布局 * 2.添加两个标签组件:时间、雷数 * 3.添加两个文本框组件:时间文本框、剩余雷数文本框 */ private void infoPanel() { infoPanel = new JPanel(); infoPanel.setBackground(Color.decode(c_info)); // 设置背景颜色 JLabel mine = new JLabel("雷数"); mine.setHorizontalAlignment(SwingConstants.CENTER); // 字体居中显示 mine.setFont(fText); // 设置字体 mineText = new JTextField(3); mineText.setEditable(false); // 设置为不可编辑 mineText.setHorizontalAlignment(SwingConstants.CENTER); mineText.setFont(infoNumber); JLabel time = new JLabel("时间"); time.setFont(fText); time.setHorizontalAlignment(SwingConstants.CENTER); timeText = new JTextField("0", 3); timeText.setEditable(false); timeText.setHorizontalAlignment(SwingConstants.CENTER); timeText.setFont(infoNumber); // 流布局的特点,组件的排序方式为添加组件的顺序。 infoPanel.add(time); infoPanel.add(timeText); infoPanel.add(mineText); infoPanel.add(mine); getContentPane().add(infoPanel, BorderLayout.SOUTH); // 将信息面板添加到窗口BorderLayout布局的下方位置 } /** * 主面板设计: * 1.采用网格布局 * 2.为每一个雷设置一个空面板,用放置按钮跟标签,采用卡片布局(卡片布局的优点在于所有组件共享一个 * 容器,并且容器只显示最上层的组件) * 3.空面板上添加标签,标签用于显示雷数(int)或者雷(Icon)。 * 4.添加空白按钮,用来遮挡标签。并添加事件,当发生点击事件时,将该按钮移除,显示出标签。 */ private void minePanel() throws Exception { minePanel = new JPanel(); // 采用Nimbus观感 UIManager.setLookAndFeel(lafNim); SwingUtilities.updateComponentTreeUI(minePanel); pack(); minePanel.setLayout(new GridLayout(16, 30)); // 设置网格布局 minePanel.setBackground(Color.decode(c_minePan)); minePanel.setBorder(BorderFactory.createEmptyBorder(10, 15, 10, 15)); // 设置一个空边框 addMinePan(); init(); getContentPane().add(minePanel, BorderLayout.CENTER); // 将主面板添加到窗口BorderLayout布局的中间位置 } /** * 添加雷面板 */ private void addMinePan() throws Exception { mineJan = new JPanel[16][30]; for (int i = 0; i < 16; i++) { for (int j = 0; j < 30; j++) { mineJan[i][j] = new JPanel(); mineJan[i][j].setLayout(new BorderLayout()); mineJan[i][j].setBackground(Color.decode(c_minePan)); minePanel.add(mineJan[i][j]); } } } /** * 添加雷按钮 */ private void addMineBut(int i, int j) { mineBut[i][j] = new JButton(); mineBut[i][j].setName(i + "_" + j); mineBut[i][j].setFocusable(false); mineBut[i][j].addActionListener(buttonAc); mineBut[i][j].addMouseListener(mouseAC); mineJan[i][j].add(mineBut[i][j]); } /** * 添加雷标签 */ private void addMineLab(int i, int j) { mineLab[i][j] = new JLabel(); mineLab[i][j].setHorizontalAlignment(JLabel.CENTER); mineLab[i][j].setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Color.BLACK)); } private void showLab(int i, int j) { mineJan[i][j].remove(mineBut[i][j]); mineJan[i][j].add(mineLab[i][j]); mineJan[i][j].updateUI(); mineJan[i][j].repaint(); } /** * 判断剩余雷数 */ private void isClean() { int mineFound = MINE_NUMBER_H; for (int i = 0; i < ROW; i++) { for (int j = 0; j < COL; j++) { if (mineFalg[i][j] == IS_FLAG) { mineFound--; } } } mineText.setText("" + mineFound); if (mineFound == 0) { flag = true; showMine(); } } /** * 显示所有的雷 */ private void showMine() { for (int i = 0; i < 16; i++) { for (int j = 0; j < 30; j++) { if (map[i][j] == MINE) { showLab(i, j); } } } } /** * 空白区自动翻开的方法: * 先检测该点是否为空,如果不是,则跳过方法。 * 如果为空,则分别检测周围的8个空格,如果周围空格有空,则递归调用该方法,直到所有的连接空白区都 * 被翻开 */ private void isBlank(int i, int j) { if (mN(i, j) == 0) { map[i][j] = CHECKED; int row = i; int col = j; for (int m = -1; m < 2; m++) { i = row; i += m; for (int n = -1; n < 2; n++) { j = col; j += n; if (i >= 0 && j >= 0 && i < 16 && j < 30) { if (mN(i, j) == 0) { if (map[i][j] != MINE && map[i][j] != CHECKED) { showLab(i, j); isBlank(i, j); } } else { showLab(i, j); // 检测到周围格子不为空,也要显示这个格子,前提这个格子不是雷 } } } } } } /** * 随机生成雷的方法: 雷的表示:可以用二维数组的下标表示,所以可以用随机数Math.random()方法来生成随机横纵坐标, * random()方法是随机生成0~1之间的小数,但是不会生成0和1.所以random()*16就表示随机生成0~16之间的浮点数,不包括0和16 * 因为坐标是整数类型,所以生成的随机横纵坐标需要强制转换为int类型 有一点需要注意的是,强制转换类型并不是四舍五入转换,例如: * random()*30生成的数的范围应该是0.0000000000000...1 ~ 29.999999999999999.... * 强制转换为int类型之后,范围会变成0 ~ 29 * * @param mineNum:表示要生成雷的总数 * @param row:表示随机雷的横坐标的int类型 * @param col:表示随机雷的纵坐标的int类型 * */ private void addMine() { for (int mineNum = 1; mineNum <= MINE_NUMBER_H;) { int row = (int) (Math.random() * 16); int col = (int) (Math.random() * 30); // 为了避免有相同位置的雷的生成,需要简单判断一下 if (map[row][col] == EMPTY) { map[row][col] = MINE; mineLab[row][col].setIcon(img(10)); mineNum++; } } } /** * 计算每个格周围八个格的雷的总数量的方法,并返回int类型的雷的数量值 */ private int mN(int i, int j) { int count = 0; // 雷的数量 int row = i; int col = j; for (int m = -1; m < 2; m++) { i = row; i += m; for (int n = -1; n < 2; n++) { j = col; j += n; if (i >= 0 && j >= 0 && i < 16 && j < 30) { if (map[i][j] == MINE) { count++; } } } } return count; } /** * 这个方法是将图片按比例缩放成40*40像素的图片,因为图片太大lable显示不完全。 当然,用文字表示显然更加的简单,比如用"*"表示雷。 */ private Icon img(int count) { Image img; switch (count) { case 0: image = new ImageIcon("null.jpg"); break; case 1: image = new ImageIcon("1.jpg"); break; case 2: image = new ImageIcon("2.jpg"); break; case 3: image = new ImageIcon("3.jpg"); break; case 4: image = new ImageIcon("4.jpg"); break; case 5: image = new ImageIcon("5.jpg"); break; case 6: image = new ImageIcon("6.jpg"); break; case 7: image = new ImageIcon("7.jpg"); break; case 8: image = new ImageIcon("8.jpg"); break; case 9: image = new ImageIcon("flag.jpg"); break; default: image = new ImageIcon("mine.jpg"); break; } img = image.getImage(); img = img.getScaledInstance(40, 40, Image.SCALE_DEFAULT); image.setImage(img); return image; } /** * 为非雷按钮设置相对应的图标 */ private void setIcon() { for (int i = 0; i < ROW; i++) { for (int j = 0; j < COL; j++) { if (map[i][j] != MINE) { int c = mN(i, j); if (c == 0) { mineLab[i][j].setIcon(img(0)); } else { mineLab[i][j].setIcon(img(c)); } } } } } /** * 计时器设计: 一开始为int t = 0; 然后利用线程休眠1000毫秒(1秒),循环增加时间 * 这个时候一定要设置一个标志用于判断这个循环什么时候跳出,如下我设置了布尔变量flag. * 这个变量是根据以下4种游戏情况而改变的: 1.默认是true,即开始游戏时是不记时的。 * 2.当点击任何一个雷按钮时,flag的值变为false,表示开始计时 3.当点击到雷时,flag的值变为true,表示游戏结束,计时停止 * 4.当清完所有非雷区是,游戏结束,flag的值变为true * 变量t用infoPanel面板中的timeText文本框进行输出 * @param t 时间 * @param flag 循环是否结束的标志 */ public void run() { int t = 0; while (true) { if (flag) { break; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } t++; timeText.setText("" + t); } } /** * 按钮点击事件 每次点击按钮,都将按钮移除,显示标签。 1.如果是雷,则游戏结束 2.如果不是雷,就调用检查周围是否为空的方法isBlank() */ private class ButtonAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { Object obj = e.getSource(); String[] name = ((JButton) obj).getName().split("_"); int i = Integer.parseInt(name[0]); int j = Integer.parseInt(name[1]); showLab(i, j); if (map[i][j] == MINE) { flag = true; showMine(); int msg = JOptionPane.showConfirmDialog(mineBut[i][j], "很不幸,你踩到雷了,是否再来一次", "消息", JOptionPane.YES_NO_OPTION); if (msg == JOptionPane.YES_OPTION) { try { init(); } catch (Exception e1) { e1.printStackTrace(); } } else { System.exit(0); } return; } isBlank(i, j); isClean(); } } private class MenuAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { String item = e.getActionCommand(); if (item.equals("新游戏(N)")) { try { init(); } catch (Exception e1) { e1.printStackTrace(); } } if (item.equals("退出(E)")) { System.exit(0); } if (item.equals("关于扫雷(A)")) { @SuppressWarnings("unused") int about = JOptionPane.showConfirmDialog(menuBar, "作者:Ranbe_Chen.\n时间:2017-9-2", "所有者", JOptionPane.YES_OPTION); } } } /** * 鼠标右键事件: 当对按钮点击右键时,先判断该按钮是否已经被标记了: 1)如果没有,则标记上旗子,并将按钮设为不可点击,剩余雷数减1; * 2)如果标记了,则将按钮旗子移去,并将按钮设为可点击,剩余雷数加1 */ private class mouseAction implements MouseListener { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON3) { Object obj = e.getSource(); JButton button = (JButton) e.getComponent(); String[] name = ((JButton) obj).getName().split("_"); int i = Integer.parseInt(name[0]); int j = Integer.parseInt(name[1]); if (mineFalg[i][j] == NO_FLAG) { button.setDisabledIcon(img(9)); button.setEnabled(false); mineFalg[i][j] = IS_FLAG; } else if (mineFalg[i][j] == IS_FLAG) { button.setIcon(null); button.setEnabled(true); mineFalg[i][j] = NO_FLAG; } isClean(); // 每次点击都需要检查雷数 } } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } } }

总得来说,这个扫雷程序还是欠缺很多功能,而且在算法设计方面感觉不是很好,还能继续优化,所以这份代码仅供学习参考

转载请注明原文地址: https://www.6miu.com/read-76688.html

最新回复(0)