6
\$\begingroup\$

I created the famous Minesweeper-game in Java, for which I used java-swing to create the GUI.

Here's the code:

Control.java: This class contains the main-method, which just opens the GUI.

import javax.swing.SwingUtilities;

public class Control {

    //Just to start GUI
    public static void main(String args[]) {
        SwingUtilities.invokeLater(Gui::new);
    }
}

Minesweeper.java: This class is responsible for creating the field, placing the mines and calculating the "neighbor-mines".

import java.util.Random;

public class Minesweeper {

    //Saves the places of the mines and the number of neighbor-mines
    private int[][] neighbors = new int[Gui.size][Gui.size]; 

    private boolean[][] memory = new boolean[Gui.size][Gui.size];

    //Places the bombs/mines randomly in the field
    public void placeBombs() {
        Random random = new Random();

        int i = 0;
        while(i < Gui.size * 3) {
            int x = random.nextInt(Gui.size);
            int y = random.nextInt(Gui.size);
            if(neighbors[x][y] == 0) {
                neighbors[x][y] = -1;
                i++;
            }
        }
    }

    //Counts the "neighbor-mines"
    public void countNeighbors() {
        for(int x = 0; x < Gui.size; x++) {
            for(int y = 0; y < Gui.size; y++) {
                memory[x][y] = true;
                if(neighbors[x][y] != -1) {
                    neighbors[x][y] = 0;
                    for(int i = x - 1; i <= x + 1; i++) {
                        for(int j = y - 1; j <= y + 1; j++) {
                            if(i >= 0 && j >= 0 && i < Gui.size && j < Gui.size ) {
                                if(neighbors[i][j] == -1) {
                                    neighbors[x][y]++;
                                }
                            }
                        }
                    }
                }   
            }
        }

    }

    public int getterNeighbors(int x, int y) {
        return neighbors[x][y];
    }

    public boolean getterMemory(int x, int y) {
        return memory[x][y];
    }

    public void setterMemory(int x, int y, boolean value) {
        memory[x][y] = value;
    }


}

Gui.java: The name is pretty self-explanatory: This class is responsible for the GUI.

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.Timer;

public class Gui {

    Minesweeper minesweeper = new Minesweeper();
    public static final int size = 15;
    private JFrame frame = new JFrame("Minesweeper");
    private JButton[][] buttons = new JButton[size][size];
    JTextField counter = new JTextField();

    final private int delay = 1000;
    private int seconds = 0;
    private int minutes = 0;
    Timer timer;

    //Timer
    final private ActionListener taskPerformer = new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
            if(seconds < 59) {
                seconds++;
            }
            else {
                minutes++;
                seconds = 0;
            }
            counter.setText(minutes + " : " + seconds);
        }
    };

    public Gui() {

        minesweeper.placeBombs();
        minesweeper.countNeighbors();

        timer = new Timer(delay, taskPerformer);
        timer.start();

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel panel = new JPanel();

        JPanel panel1 = new JPanel();

        //Creates the buttons
        for(int i = 0; i < size; i++) {
            for(int j = 0; j < size; j++) {
                buttons[i][j] = new JButton();
                buttons[i][j].setText("");
                buttons[i][j].setBackground(Color.GRAY);
                buttons[i][j].setName(i + "" + j);
                buttons[i][j].setBorder(BorderFactory.createLineBorder(Color.BLACK));
                final int x = i;
                final int y = j;

                //Right-click
                buttons[i][j].addMouseListener(new MouseAdapter() {
                    boolean test = true;
                    public void mouseClicked(MouseEvent e ) {

                        if(SwingUtilities.isRightMouseButton(e) && buttons[x][y].isEnabled()) {
                            if(test) {
                                buttons[x][y].setBackground(Color.ORANGE);
                                test = !test;
                            }
                            else {
                                buttons[x][y].setBackground(Color.GRAY);
                                test = !test;
                            }
                        }
                    }
                });

                //Left-click
                buttons[i][j].addActionListener(
                        new ActionListener() {
                            public void actionPerformed(ActionEvent e) {
                                buttonClicked(x, y);
                            }
                        });

                panel1.add(buttons[i][j]);
            }
        }

        //Layout
        frame.setSize(600,450);
        panel.setLayout(new java.awt.FlowLayout());
        panel.setSize(600, 400);

        panel1.setLayout(new java.awt.GridLayout(size, size));
        panel1.setPreferredSize(new Dimension(400, 400));
        JPanel panel2 = new JPanel();
        panel2.setLayout(new java.awt.GridLayout(2,1, 100, 100));
        panel2.setSize(200,400);

        //Restart button
        JButton restart = new JButton();
        restart.addActionListener(
                new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        frame.setVisible(false);
                        frame.dispose();
                        SwingUtilities.invokeLater(Gui::new);
                    }
                });

        restart.setText("Restart");

        //More Layout
        counter.setHorizontalAlignment(JTextField.CENTER);
        counter.setEditable(false);
        counter.setText("Counter");
        Font font1 = new Font("SansSerif", Font.BOLD, 30);
        counter.setFont(font1);

        restart.setFont(font1);

        panel2.add(counter);
        panel2.add(restart);
        panel.add(panel1, FlowLayout.LEFT);
        panel.add(panel2);


        frame.add(panel);

        frame.setVisible(true);
    }

    private void buttonClicked(int i, int j) {

        buttons[i][j].setEnabled(false);
        buttons[i][j].setBackground(Color.WHITE);

        if(minesweeper.getterNeighbors(i, j) == -1) {
            youLost(i, j);
        }

        else if(minesweeper.getterNeighbors(i, j) == 0) {
            zeroNeighbors(i, j);
            checkWin();
        }

        else {
            buttons[i][j].setText(Integer.toString(minesweeper.getterNeighbors(i, j)));
            checkWin();
        }

    }

    //Recursive function to reveal more fields
    private void zeroNeighbors(int x, int y) {
        minesweeper.setterMemory(x, y, false);
        for(int i = x - 1; i <= x + 1; i++) {
            for(int j = y - 1; j <= y + 1; j++) {
                if(i >= 0 && j >= 0 && i < Gui.size && j < Gui.size) {
                    buttons[i][j].setEnabled(false);      
                    buttons[i][j].setBackground(Color.WHITE);
                    buttons[i][j].setText(Integer.toString(minesweeper.getterNeighbors(i, j)));
                    if(minesweeper.getterNeighbors(i, j) == 0  && minesweeper.getterMemory(i, j)) {
                        zeroNeighbors(i, j);
                    }
                }
            }
        }
    }

    private void youLost(int x, int y) {

        for(int i = 0; i < size; i++) {
            for(int j = 0; j < size; j++) {
                buttons[i][j].setEnabled(false);
                buttons[i][j].setBackground(Color.WHITE);               
            }
        }
        timer.stop();
        buttons[x][y].setBackground(Color.RED);
        JOptionPane.showMessageDialog(frame, "You Lost!");
    }

    private void checkWin() {
        boolean test = true;
        for(int i = 0; i < size; i++) {
            for(int j = 0; j < size; j++) {
                if(buttons[i][j].isEnabled() && minesweeper.getterNeighbors(i, j) != -1) {
                    test = false;
                }
            }
        }
        if(test) {
            for(int i = 0; i < size; i++) {
                for(int j = 0; j < size; j++) {
                    buttons[i][j].setEnabled(false);
                    buttons[i][j].setBackground(Color.WHITE);
                }
            }
            timer.stop();
            JOptionPane.showMessageDialog(frame, "You Won!");

        }
    }
}

I would appreciate any suggestions on improving the code and especially the general code-structure.

\$\endgroup\$
1

1 Answer 1

3
\$\begingroup\$

To review your code, I opened it in IntelliJ, which is an integrated development environment (IDE). One of its main features is the thousands of inspections it has for making code simpler and shorter. For example, it suggests:

  • In Control.java, instead of writing String args[], the usual way is to write String[] args. Changing this does not affect the code execution in any way, it only makes the code easier to read for humans.

  • In Minesweeper.java, instead of writing private int[][], you can write private final int[][] to document that this variable is only ever assigned once, which also helps the human reader since otherwise this variable might be modified in any of the other 60 lines.

  • In Gui.java, instead of writing new ActionListener() { … }, you can replace that code with a much shorter form, which is called a lambda expression. That's a terribly unhelpful name if you don't know what it is about. A much better name is unnamed method, or in some other programming languages, anonymous function. Basically it's just a piece of code that can be run.

So much for the simple transformations. Having these code transformations at your finger tips makes it easy to experiment with your code and apply these suggestions from the IDE, as well as undo them if you don't like them.

An IDE can also format the source code, so that it has a consistent look that is familiar to many other readers. For example, in your code you write for(int i, while the common form is to have a space after the for, which makes it for (int i.

On a completely different topic, the label that displays the elapsed time sometimes jumps around on the screen. This is because the seconds "0" is thinner than the seconds "00". To avoid this, you can replace this code:

counter.setText(minutes + " : " + seconds);

with this code:

counter.setText(String.format("%d : %02d", minutes, seconds));

The String.format function is quite powerful. It defines a format with placeholders, into which the remaining arguments are inserted. In this case it means:

  • %d is just a decimal number.
  • %02d is a decimal number, but with at least 2 digits. Any number thinner than this will be filled with 0.

See this article for other popular programs that didn't get this right, there are even some programs by Apple.

When I saw your code first, I was a bit disappointed that the Minesweeper class uses the constant Gui.size. That constant has nothing to do with the GUI, it should rather be defined in the Minesweeper class, since it is not specific to the screen representation but rather to the abstract representation of the mine field.

It would also be nice if I could have a Minesweeper object with different sizes. To do this, you can edit the Minesweeper class in these steps:

  1. At the top of the class, modify the code to be:

    public class Minesweeper {
    
        private final int width;
        private final int height;
    
        public Minesweeper(int width, int height) {
            this.width = width;
            this.height = height;
        }
    
  2. Replace Gui.size with widthOrHeight everywhere in Minesweeper.java.
  3. Replace each instance of widthOrHeight with either width or height, whichever fits.
  4. Finally make the width and the height of the mine field publicly available by adding these methods at the bottom of the Minesweeper class:
    public int getWidth() { return width; }
    public int getHeight() { return height; }
    

Now you can define mine fields of arbitrary sizes.

There's certainly more to say, but I'll leave that to the other reviewers.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.