首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >基于Swing GUI的Java扫雷游戏

基于Swing GUI的Java扫雷游戏
EN

Code Review用户
提问于 2019-03-09 10:14:31
回答 2查看 6.9K关注 0票数 2

我创建了一个Java Swing Minesweeper,我希望得到一些反馈。注:我只是在高中,所以请原谅任何‘愚蠢’的方式,我可能有编码的某些部分。

任何关于如何提高代码的一般质量的技巧或书籍,或者任何帮助我变得更好的信息,都会受到深深的赞赏。

除了反馈之外,我还想知道如何在我禁用的按钮中保留颜色。在这样的上下文中,为了使已经单击的单元格再次不可点击,我刚刚禁用了它。或者,是否有更好的方法来完成同样的任务?

守则的蓝图是:

  1. Real (按钮undefined供用户单击)。
  2. MyBoard (后端用于配置每个单元的数量,等等).
  3. 方法来处理游戏中的每个事件。

请原谅语言上的歧义。

代码语言:javascript
复制
package MinesweeperGame;


//Following is the implementation of Minesweeper.



import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.util.*;

public class Minesweeper extends JFrame implements ActionListener, MouseListener{


    JFrame frame = new JFrame();               
    JButton reset = new JButton("Reset");       //Reset Button as a side.
    JButton giveUp = new JButton("Give Up");    //Similarly, give up button.  
    JPanel ButtonPanel = new JPanel();       
    Container grid = new Container();           
    int[][] counts;                             //integer array to store counts of each cell. Used as a back-end for comparisons.
    JButton[][] buttons;                        //Buttons array to use as a front end for the game.
    int size,diff;                              
    final int MINE = 10;                        

    /**
    @param size determines the size of the board
    */

    public Minesweeper(int size){
     super("Minesweeper");                       

     this.size = size;   
     counts = new int[size][size];
     buttons = new JButton[size][size];  

     frame.setSize(900,900);                       
     frame.setLayout(new BorderLayout());           
     frame.add(ButtonPanel,BorderLayout.SOUTH);     
     reset.addActionListener(this);                 
     giveUp.addActionListener(this);                


     grid.setLayout(new GridLayout(size,size));    

     for(int a = 0; a < buttons.length; a++)
     {
         for(int b = 0; b < buttons[0].length; b++)
         {
             buttons[a][b] = new JButton();            
             buttons[a][b].addActionListener(this);     
             grid.add(buttons[a][b]);                  
         }
     }
     // above initializes each button in the minesweeper board and gives it functionality. 

     ButtonPanel.add(reset);                        
     ButtonPanel.add(giveUp);       // adding buttons to the panel.

     frame.add(grid,BorderLayout.CENTER);   
     createMines(size);                         //calling function to start the game by filling mines.

     frame.setLocationRelativeTo(null);      
     frame.setDefaultCloseOperation(EXIT_ON_CLOSE);     //frame stuff
     frame.setVisible(true);

    }
    /**
     * Function to check whether user has lost the game ( i.e clicked a mine).
     * @param m indicated whether the function has been called when user clicks a mine( m=1)
     * or when he clicks the give up button.(m = any other integer).
     * Shows a dialog box which tells the user that they have lost the game.
     */
    public void takeTheL(int m){

        for(int x = 0; x < size; x++)
        {
            for(int y = 0; y < size; y++)
            {
                if(buttons[x][y].isEnabled())          // when a button has been clicked, it is disabled.
                {
                    if(counts[x][y] != MINE)
                    {
                        buttons[x][y].setText(""+ counts[x][y]);                    
                    }

                    else
                    {
                        buttons[x][y].setText("X");

                    }
                    buttons[x][y].setEnabled(false);
                }
            }
        }
    JOptionPane.showMessageDialog(null, m==1? "You clicked a mine!":"You Gave Up",
                                 "Game Over", JOptionPane.ERROR_MESSAGE);
    } 
    /**
     * Function to check whether user has won or not
     * It performs this by checking whether a cell that is NOT a mine
     * remains to be clicked by the user.
     * (Works because, when a user clicks a button, it is disabled to avoid further moves on the same cell).
     * Function prints a pop-up message congratulating user on victory.
     */

    public void takeTheW() {
       boolean won = true;
       for(int i = 0; i < size; i++)
       {
           for(int j = 0; j < size; j++)
           {
               if(counts[i][j] != MINE && buttons[i][j].isEnabled())
               {
                   won = false;
               }
           }
       }
       if(won) 
       {
            JOptionPane.showMessageDialog(null,"You have won!", "Congratulations!",
                                          JOptionPane.INFORMATION_MESSAGE);
       }   
    }



    @Override
    public void actionPerformed(ActionEvent ae) {
        if(ae.getSource() == reset)              //resets grid
        {
            for(int x = 0; x < size; x++)
            {
                for(int y = 0; y < size; y++)
                {
                    buttons[x][y].setEnabled(true);
                    buttons[x][y].setText("");
                }
            }
            createMines(30);  //triggers a new game.
        }

        else if(ae.getSource() == giveUp)  //user has given up. trigger takeTheL( m!= 1).
        {
                   takeTheL(0); // anything not = 1
        }

        else // click was on a cell
        {
                for(int x = 0; x < size; x++)
                {
                    for( int y = 0; y < size; y++)
                    {
                        if(ae.getSource() == buttons[x][y])
                        {
                            switch (counts[x][y]) {
                                case MINE:
                                    buttons[x][y].setForeground(Color.RED);
                                    buttons[x][y].setIcon(new ImageIcon("")); // add bomb image
                                    takeTheL(1);                                    //user clicked on a mine
                                    break;
                                case 0:
                                    buttons[x][y].setText(counts[x][y] +"");
                                    buttons[x][y].setEnabled(false);
                                    ArrayList<Integer> clear = new ArrayList<>();    
                                    clear.add(x*100+y);
                                    dominoEffect(clear); // To recursively clear all surrounding '0' cells.
                                    takeTheW(); //checks win every move
                                    break;
                                default:
                                    buttons[x][y].setText(""+counts[x][y]);
                                    buttons[x][y].setEnabled(false);
                                    takeTheW();                                          // its a number > 0 and not a mine, so just check for win
                                    break;
                            }
                        }    
                    }
                }
        }


    }
    /**
     * Function creates mines at random positions.
     * @param s the size of the board(row or column count)
     */

    public void createMines(int s){
    ArrayList<Integer> list = new ArrayList<>();  //Modifiable array to store pos. of mines.
        for(int x = 0; x < s; x++)
        {
            for(int y = 0; y < s; y++)
            {
                list.add(x*100+y);                       // x & y shall be individually retrieved by dividing by 100 and modulo 100 respectively.
                                                         // refer to lines 284 and 285 for implementation
            }
        }
        counts = new int[s][s];                    //resetting back-end array

        for(int a = 0; a < (int)(s * 1.5); a++)
        {
            int choice = (int)(Math.random() * list.size());
            counts [list.get(choice) / 100] [list.get(choice) % 100] = MINE;      //Using corollary of before-last comment to set mines as well.
            list.remove(choice);                                                                           // We don't want two mines in the same pos., so remove that pos. from list.
        }
        /*
        Following segment initializes 'neighbor counts' for each cell. That is, the number of 
        mines that are present in the eight surrounding cells. IF the cell isn't a mine.
        Note : It is done in the back-end array as that contains the numbers (MINE or 1 or 2 or 3 or 4 or 5 or 6 or 7 or 8)
        */
        for(int x = 0; x < s; x++)
        {
           for(int y = 0; y < s; y++)
           {
            if(counts[x][y] != MINE)
            {
                int neighbor = 0;
                if( x > 0 && y > 0 && counts[x-1][y-1] == MINE) //top left
                {
                    neighbor++;
                }
                if( y > 0 && counts[x][y-1] == MINE) //left
                {
                    neighbor++;
                }
                if( y < size - 1 && counts[x][y+1] == MINE) //right
                {
                    neighbor++;
                }
                if( x < size - 1 && y > 0 && counts[x+1][y-1] == MINE) //bottom left
                {
                    neighbor++;
                }
                if( x > 0 && counts[x-1][y] == MINE) //up
                {
                    neighbor++;
                }
                if( x < size - 1 && counts[x+1][y] == MINE)//down
                {
                    neighbor++;
                }
                if( x > 0 && y < size - 1 &&counts[x-1][y+1] == MINE) //top right
                {
                    neighbor++;
                }
                if( x < size - 1 && y < size - 1 && counts[x+1][y+1] == MINE) //bottom right
                {
                    neighbor++;
                }
                counts[x][y] = neighbor;                        //setting value
            }
           }
        }
    }


    /**
     * This function, called the domino effect, is an implementation of the idea that,
     * when a cell with no surrounding mines is clicked, there's no point in user clicking
     * all eight surrounding cells. Therefore, all surrounding
     * cells' counts will be displayed in corresponding cells. 
     * The above is done recursively.
     * @param toClear the ArrayList which is passed to the function with positions in array
     *                that are zero, and are subsequently clicked.
     */

    public void dominoEffect(ArrayList<Integer> toClear){
        if(toClear.isEmpty())
            return;                         //base case

        int x = toClear.get(0) / 100;       //getting x pos.
        int y = toClear.get(0) % 100;       //getting y pos.
        toClear.remove(0);                  //remove that element from array to prevent infinite recursion.    
            if(counts[x][y] == 0)
            {                               //similar to neighbor counts, each surrounding cell is filled   

                if( x > 0 && y > 0 && buttons[x-1][y-1].isEnabled()) //top left
                {
                    buttons[x-1][y-1].setText(counts[x-1][y-1] + "");
                    buttons[x-1][y-1].setEnabled(false);
                    if(counts[x-1][y-1] == 0)
                    {
                        toClear.add((x-1)*100 + (y-1));     //to recursively implement, each surrounding cell is the new cell,
                                                                              // the surrounding cells of which we shall check and so on.
                    }
                }
                if( y > 0 && buttons[x][y-1].isEnabled()) //left
                {
                    buttons[x][y-1].setText(counts[x][y-1] + "");
                    buttons[x][y-1].setEnabled(false);
                    if(counts[x][y-1] == 0)
                    {
                        toClear.add(x*100 + (y-1));
                    }

                }
                if( y < size - 1 && buttons[x][y+1].isEnabled()) //right
                {
                    buttons[x][y+1].setText(counts[x][y+1] + "");
                    buttons[x][y+1].setEnabled(false);
                    if(counts[x][y+1] == 0)
                    {
                        toClear.add(x*100 + (y+1));
                    }

                }
                if( x < size - 1 && y > 0 && buttons[x+1][y-1].isEnabled()) //bottom left
                {
                    buttons[x+1][y-1].setText(counts[x+1][y-1] + "");
                    buttons[x+1][y-1].setEnabled(false);
                    if(counts[x+1][y-1] == 0)
                    {
                        toClear.add((x+1)*100 + (y-1));
                    }

                }
                if( x > 0 && buttons[x-1][y].isEnabled()) //up
                {
                    buttons[x-1][y].setText(counts[x-1][y] + "");
                    buttons[x-1][y].setEnabled(false);
                    if(counts[x-1][y] == 0)
                    {
                        toClear.add((x-1)*100 + y);
                    }

                }
                if( x < size - 1 && buttons[x+1][y].isEnabled())//down
                {
                    buttons[x+1][y].setText(counts[x+1][y] + "");
                    buttons[x+1][y].setEnabled(false);
                    if(counts[x+1][y] == 0)
                    {
                        toClear.add((x+1)*100 + y);
                    }

                }
                if( x > 0 && y < size - 1 && buttons[x-1][y+1].isEnabled()) //top right
                {
                    buttons[x-1][y+1].setText(counts[x-1][y+1] + "");
                    buttons[x-1][y+1].setEnabled(false);
                    if(counts[x-1][y+1] == 0)
                    {
                        toClear.add((x-1)*100 + (y+1));
                    }

                }
                if( x < size - 1 && y < size - 1 && buttons[x+1][y+1].isEnabled()) //bottom right
                {
                    buttons[x+1][y+1].setText(counts[x+1][y+1] + "");
                    buttons[x+1][y+1].setEnabled(false);
                    if(counts[x+1][y+1] == 0)
                    {
                        toClear.add((x+1)*100 + (y+1));
                    }

                }
            }
            dominoEffect(toClear);      //recursive call with list containing surrounding cells, for further check-and-clear of THEIR surr. cells.
    }

    //Main method.
    public static void main(String[] args){
        new Minesweeper(20);    // Can be made of any size. (For now only squares)


    }

    @Override
    public void mouseClicked(MouseEvent me) {
        if (SwingUtilities.isRightMouseButton(me)){
            // TODO : Handle flagging of mines.
        }
    }

    @Override
    public void mousePressed(MouseEvent me) {
     // Do nothing
    }

    @Override
    public void mouseReleased(MouseEvent me) {
     // Do nothing
    }

    @Override
    public void mouseEntered(MouseEvent me) {
        // Do nothing
    }

    @Override
    public void mouseExited(MouseEvent me) {
       // Do nothing
    }

}
EN

回答 2

Code Review用户

回答已采纳

发布于 2019-03-11 15:49:58

Introduction

你的回答让我想起了我在高中的时候,用秋千之类的东西写你写的东西。我决定重新过这段时间,并花了相当多的时间写一个全面的答案,并解释所有的重大决定。

上的改进

  1. 您实现了MouseListener,但从未实际使用它。我会把它完全移除。使用ActionListener足够。除非您添加了标记地雷的功能。这似乎是未来的一个补充。继续阅读,看到一个更优雅的解决方案。
  2. 扫雷舰扩展了JFrame,但最终使用了本地的。从一开始继承它有什么用?
  3. 您可以维护两个单独的数组来维护计数和按钮。如果在这里应用OOP,可以大大提高代码的一般可读性及其易用性(和维护性)。我的解决方案是创建一个名为Cell的类,它扩展了JButton,并添加了位置和计数存储。
  4. (x, y)坐标对映射到整数的方式是非一般的。为什么是100?这似乎是武断的。实际上,您可以通过使用size而不是100来实现相同的事情(唯一的映射)。它还可以推广到网格的大小超出100的情况,在这种情况下,使用固定的常量将停止产生唯一的数字。
  5. 代码有很多重复。特别是当你检查一个细胞的邻居是地雷还是零值的时候。我给出了一个解决方案,您可以获得所有有效的邻居并对它们执行操作。
  6. dominoEffect方法的命名不是按照将方法命名为动词或动词短语的准则。cascade是我用的那个。另外,takeTheWtakeTheL可能还行,但我不喜欢它们。总是更喜欢描述方法正在做什么的名称。
  7. 您实现的递归是尾递归,这意味着可以用适当的循环替换它,从而避免了大量开销。
  8. ActionLister (以及稍后的MouseListener)接口可以匿名实现,并存储在变量中。这减少了杂乱。
  9. 当你只为整数选择两种可能的结果时,你更喜欢布尔。例如:m中的参数takeTheL()
  10. 使用更好的数据结构,当您依赖于经常执行的操作时,才能有效地执行。我建议用集合代替你的数组列表。
  11. Swing应用程序应该在一个单独的线程上启动。请参考重构代码中的main方法。
  12. 还有更多的内容,但我已经将它们全部包含在我在下一节中介绍的重构代码中。

重构程序

无可否认,我对这些评论略知一二,但我相信这段代码是可读的和不言自明的。如果您不理解某个特定片段,请留下评论。

代码语言:javascript
复制
package minesweeperimproved;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.util.*;

/**
 * This is the refactored version of the code presented in
 * this post at CodeReview.SE:
 * <p>
 * https://codereview.stackexchange.com/questions/215081/created-a-minesweeper-game-on-java-using-swing-gui-i-wish-to-undertake-improvem
 * <p>
 * Original author: greyothello (https://codereview.stackexchange.com/users/194786/greyothello)
 * Refactored by: HungryBlueDev (https://codereview.stackexchange.com/users/37479/hungry-blue-dev)
 */
public class Minesweeper {
    // The value assigned to cells marked as mines. 10 works
    // because no cell will have more than 8 neighbouring mines.
    private static final int MINE = 10;
    // The size in pixels for the frame.
    private static final int SIZE = 500;

    // The number of mines at generated is the grid size * this constant
    private static final double POPULATION_CONSTANT = 1.5;

    // This fixed amount of memory is to avoid repeatedly declaring
    // new arrays every time a cell's neighbours are to be retrieved.
    private static Cell[] reusableStorage = new Cell[8];

    private int gridSize;

    private Cell[][] cells;

    private JFrame  frame;
    private JButton reset;
    private JButton giveUp;

    private final ActionListener actionListener = actionEvent -> {
        Object source = actionEvent.getSource();
        if (source == reset) {
            createMines();
        } else if (source == giveUp) {
            revealBoardAndDisplay("You gave up.");
        } else {
            handleCell((Cell) source);
        }
    };

    private class Cell extends JButton {
        private final int row;
        private final int col;
        private       int value;

        Cell(final int row, final int col,
             final ActionListener actionListener) {
            this.row = row;
            this.col = col;
            addActionListener(actionListener);
            setText("");
        }

        int getValue() {
            return value;
        }

        void setValue(int value) {
            this.value = value;
        }

        boolean isAMine() {
            return value == MINE;
        }

        void reset() {
            setValue(0);
            setEnabled(true);
            setText("");
        }

        void reveal() {
            setEnabled(false);
            setText(isAMine() ? "X" : String.valueOf(value));
        }

        void updateNeighbourCount() {
            getNeighbours(reusableStorage);
            for (Cell neighbour : reusableStorage) {
                if (neighbour == null) {
                    break;
                }
                if (neighbour.isAMine()) {
                    value++;
                }
            }
        }

        void getNeighbours(final Cell[] container) {
            // Empty all elements first
            for (int i = 0; i < reusableStorage.length; i++) {
                reusableStorage[i] = null;
            }

            int index = 0;

            for (int rowOffset = -1; rowOffset <= 1; rowOffset++) {
                for (int colOffset = -1; colOffset <= 1; colOffset++) {
                    // Make sure that we don't count ourselves
                    if (rowOffset == 0 && colOffset == 0) {
                        continue;
                    }
                    int rowValue = row + rowOffset;
                    int colValue = col + colOffset;

                    if (rowValue < 0 || rowValue >= gridSize
                        || colValue < 0 || colValue >= gridSize) {
                        continue;
                    }

                    container[index++] = cells[rowValue][colValue];
                }
            }
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass())
                return false;
            Cell cell = (Cell) obj;
            return row == cell.row &&
                   col == cell.col;
        }

        @Override
        public int hashCode() {
            return Objects.hash(row, col);
        }
    }

    private Minesweeper(final int gridSize) {
        this.gridSize = gridSize;
        cells = new Cell[gridSize][gridSize];

        frame = new JFrame("Minesweeper");
        frame.setSize(SIZE, SIZE);
        frame.setLayout(new BorderLayout());

        initializeButtonPanel();
        initializeGrid();

        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    private void initializeButtonPanel() {
        JPanel buttonPanel = new JPanel();

        reset = new JButton("Reset");
        giveUp = new JButton("Give Up");

        reset.addActionListener(actionListener);
        giveUp.addActionListener(actionListener);

        buttonPanel.add(reset);
        buttonPanel.add(giveUp);
        frame.add(buttonPanel, BorderLayout.SOUTH);
    }

    private void initializeGrid() {
        Container grid = new Container();
        grid.setLayout(new GridLayout(gridSize, gridSize));

        for (int row = 0; row < gridSize; row++) {
            for (int col = 0; col < gridSize; col++) {
                cells[row][col] = new Cell(row, col, actionListener);
                grid.add(cells[row][col]);
            }
        }
        createMines();
        frame.add(grid, BorderLayout.CENTER);
    }

    private void resetAllCells() {
        for (int row = 0; row < gridSize; row++) {
            for (int col = 0; col < gridSize; col++) {
                cells[row][col].reset();
            }
        }
    }

    private void createMines() {
        resetAllCells();

        final int    mineCount = (int) POPULATION_CONSTANT * gridSize;
        final Random random    = new Random();

        // Map all (row, col) pairs to unique integers
        Set<Integer> positions = new HashSet<>(gridSize * gridSize);
        for (int row = 0; row < gridSize; row++) {
            for (int col = 0; col < gridSize; col++) {
                positions.add(row * gridSize + col);
            }
        }

        // Initialize mines
        for (int index = 0; index < mineCount; index++) {
            int choice = random.nextInt(positions.size());
            int row    = choice / gridSize;
            int col    = choice % gridSize;
            cells[row][col].setValue(MINE);
            positions.remove(choice);
        }

        // Initialize neighbour counts
        for (int row = 0; row < gridSize; row++) {
            for (int col = 0; col < gridSize; col++) {
                if (!cells[row][col].isAMine()) {
                    cells[row][col].updateNeighbourCount();
                }
            }
        }
    }

    private void handleCell(Cell cell) {
        if (cell.isAMine()) {
            cell.setForeground(Color.RED);
            cell.reveal();
            revealBoardAndDisplay("You clicked on a mine!");
            return;
        }
        if (cell.getValue() == 0) {
            Set<Cell> positions = new HashSet<>();
            positions.add(cell);
            cascade(positions);
        } else {
            cell.reveal();
        }
        checkForWin();
    }

    private void revealBoardAndDisplay(String message) {
        for (int row = 0; row < gridSize; row++) {
            for (int col = 0; col < gridSize; col++) {
                if (!cells[row][col].isEnabled()) {
                    cells[row][col].reveal();
                }
            }
        }

        JOptionPane.showMessageDialog(
                frame, message, "Game Over",
                JOptionPane.ERROR_MESSAGE
        );

        createMines();
    }

    private void cascade(Set<Cell> positionsToClear) {
        while (!positionsToClear.isEmpty()) {
            // Set does not have a clean way for retrieving
            // a single element. This is the best way I could think of.
            Cell cell = positionsToClear.iterator().next();
            positionsToClear.remove(cell);
            cell.reveal();

            cell.getNeighbours(reusableStorage);
            for (Cell neighbour : reusableStorage) {
                if (neighbour == null) {
                    break;
                }
                if (neighbour.getValue() == 0
                    && neighbour.isEnabled()) {
                    positionsToClear.add(neighbour);
                } else {
                    neighbour.reveal();
                }
            }
        }
    }

    private void checkForWin() {
        boolean won = true;
        outer:
        for (Cell[] cellRow : cells) {
            for (Cell cell : cellRow) {
                if (!cell.isAMine() && cell.isEnabled()) {
                    won = false;
                    break outer;
                }
            }
        }

        if (won) {
            JOptionPane.showMessageDialog(
                    frame, "You have won!", "Congratulations",
                    JOptionPane.INFORMATION_MESSAGE
            );
        }
    }

    private static void run(final int gridSize) {
        try {
            // Totally optional. But this applies the look and
            // feel for the current OS to the a application,
            // making it look native.
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception ignore) { }
        // Launch the program
        new Minesweeper(gridSize);
    }

    public static void main(String[] args) {
        final int gridSize = 10;
        SwingUtilities.invokeLater(() -> Minesweeper.run(gridSize));
    }
}
票数 2
EN

Code Review用户

发布于 2019-03-11 07:26:47

第一步是研究MVC设计模式。现在,所有代码都在一个文件中。将游戏的逻辑分离成一个独立的“扫雷引擎”类,这个类不依赖于Swing框架。使引擎将事件发送到用户界面。

至于代码风格,请看一下这里的一些Java标记文章,了解如何格式化代码和声明变量(您没有遵循太多已知的良好实践)。

票数 0
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/215081

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档