5
\$\begingroup\$

Can you please review my Minesweeper implementation in Java?

package com.game;

public class Tile {
    private boolean hasMine = false;
    private boolean revealed = false;
    private int mineCount = 0;
    
    
    public boolean hasMine() {
        return hasMine;
    }
    public void setHasMine(boolean hasMine) {
        this.hasMine = hasMine;
    }
    public boolean isRevealed() {
        return revealed;
    }
    public void setRevealed(boolean revealed) {
        this.revealed = revealed;
    }
    public int getMineCount() {
        return mineCount;
    }
    public void setMineCount(int mineCount) {
        this.mineCount = mineCount;
    }
    
    
}
package com.game;

import java.util.Random;

public class Board {
    private Tile[][] tiles;
    private int N = 0;
    private int totalRevealed = 0;
    
    private static int[][] neighbors = {{-1,-1}, {-1,0}, {-1,1}, {0,-1}, {0,1}, {1, 0}, {1, 1}, {1, -1}};
    
    public void initialize(int n, int m) throws Exception {
        this.N = n;
        tiles = new Tile[n][n];
        
        // If there are more mines to place than the total tiles, throw an exception
        if (m > n*n) {
            throw new Exception("Invalid mines.");
        }
        
        // Initialize each tile in the grid
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                
                tiles[i][j] = new Tile();
            }
        }
        
        placeMines(m); // time to place the mines
        
    }
    
    /*
        Randomly places mines.
    */
    private void placeMines(int m) {
        // Need to generate random row and column values to place the mine
        Random rand1 = new Random(), rand2 = new Random();
        
        while (m > 0) {
            int i = rand1.nextInt(this.N), j = rand2.nextInt(this.N);
            //System.out.println("i: " + i + "| j: " + j);
                           
            if (!tiles[i][j].hasMine()) {
                tiles[i][j].setHasMine(true);
                
                for (int[] nei : neighbors) {
                    
                    if (i + nei[0] < 0 || j + nei[1] < 0 || i + nei[0] >= this.N || j + nei[1] >= this.N) continue;
                    
                    if (!tiles[i+nei[0]][j+nei[1]].hasMine()) {
                        tiles[i+nei[0]][j+nei[1]].setMineCount(tiles[i+nei[0]][j+nei[1]].getMineCount()+1);
                    }
                }
                
                m--;
            }
        }
    }
    
    public void printBoard() {
        for (Tile[] tile : tiles) {
            
            for (Tile t : tile) {
                if (t.hasMine() && t.isRevealed()){
                    System.out.print("\t " + "*");
                    
                }else {
                 if (t.isRevealed()) {
                    System.out.print("\t " + t.getMineCount());
                 } else {
                     System.out.print("\t -");
                 }
                }
                
            }
            
            System.out.println();
            
        }
    }
    
    /*
     * If clicked on a mine, reveal all the mines and then it's game over.
     * 
     * If revealed all the tiles without stepping on a mine, it's game over too.
     */
    public boolean click(int i, int j) {
        
        if (tiles[i][j].isRevealed()) return false;
        
        if (tiles[i][j].hasMine()) {
            revealAllMines();
            return true;
        }
        
        reveal(tiles[i][j], i, j);
        
        return totalRevealed == (this.N * this.N);
    }
    
    /*
     * Reveals all possible neighbors and their neighbors recursively.
     * 
     */
    private void reveal(Tile curr, int i, int j) {
        
        if (curr.getMineCount() == 0) { // reveal neighboring tiles
            curr.setRevealed(true);
            totalRevealed++;
            
            for (int[] nei : neighbors) {
                if (i + nei[0] < 0 || j + nei[1] < 0 || i + nei[0] >= this.N || j + nei[1] >= this.N) continue;
                
                if (!tiles[i+nei[0]][j+nei[1]].hasMine() && !tiles[i+nei[0]][j+nei[1]].isRevealed()) {
                    //tiles[i+nei[0]][j+nei[1]].revealed = true;
                    reveal(tiles[i+nei[0]][j+nei[1]], i+nei[0], j+nei[1]);
                    
                }
                    
            }
            
        } else {
            curr.setRevealed(true);
        }

    }
    
    private void revealAllMines() {
        for (Tile[] tile : tiles) {
            
            for (Tile t : tile) {
                
                if (t.hasMine()) {
                    t.setRevealed(true);
                    totalRevealed++;
                }
                
            }
        }
    }
    
}
package com.game;

import java.util.Scanner;

/*
 * 
 * Implemented Minesweeper game.
 * 
 */

public class Minesweeper {

    public static void main(String[] args) {
        Board board = new Board();

        try {
            Scanner scanner = new Scanner(System.in);

            int n = 0, m = 0;
            do {
                System.out.println("Enter the grid size (less than or equal to 30): ");
                n = scanner.nextInt();

                System.out.println("Enter the number of mines: ");
                m = scanner.nextInt();
            } while (n > 30);

            board.initialize(n, m);

            System.out.println("Initialized board:");
            board.printBoard();

            boolean gameOver = false;

            int row = 0, col = 0;
            do {
                System.out.println("Row?");
                row = scanner.nextInt();

                System.out.println("Column?");
                col = scanner.nextInt();

                if (row == -1 && col == -1) {
                    System.exit(0);
                }

                if ((row >= 0 && row < n) && (col >= 0 && col < n)) {
                    gameOver = board.click(row, col);

                    if (gameOver) {
                        System.out.println("\n\nGame Over");
                        board.printBoard();
                        System.exit(0);

                    } else {
                        System.out.println("\n\n");
                        board.printBoard();
                        System.out.println("\n\n");
                    }
                }

            } while (!gameOver);

        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

Implemented Minesweeper game without a graphical interface.

Steps to run the game:

I have created Minesweeper.java class. Run it and it will ask you to enter the grid size, mines, row and column on every play.

Currently, you cannot create a grid larger than 30 x 30 tiles.

The total mines cannot exceed the total tiles in the grid.

Rules to play:

  1. Create a board of size n x n tiles.
  2. Enter the number of mines to place on the board randomly.
  3. Enter valid row number and column number (0 based index).
  4. If you step on a mine, it's game over and all the mines are revealed.
  5. You can win by not stepping on any mine and reveal all the safe tiles.

Please let me know your feedback.

Thanks.

\$\endgroup\$
3
\$\begingroup\$

Quite nicely done, actually.


It's interesting regarding how you name things, you either very good names, or useless ones, there is no in-between.

    private int N = 0;

    //...

    public void initialize(int n, int m) throws Exception {
        this.N = n;
        tiles = new Tile[n][n];
        
        // If there are more mines to place than the total tiles, throw an exception
        if (m > n*n) {
            throw new Exception("Invalid mines.");
        }
        
        // Initialize each tile in the grid
        for (int i=0; i<n; i++) {
            for (int j=0; j<n; j++) {
                
                tiles[i][j] = new Tile();
            }
        }
        
        placeMines(m); // time to place the mines
        
    }

So much readability could be gained by using "columns", "rows", "column", "row" or "x" and "y" here is variable names. "m" could be called "mineCount" or similar.

I have roughly two rules when it comes to naming things:

  1. Name it after what it does or contains.
  2. You're only allowed to use single-letter names if you're dealing with dimensions. Yes, that includes loop counters.

    /*
        Randomly places mines.
    */
    private void placeMines(int m) {

Javadoc


        // Need to generate random row and column values to place the mine
        Random rand1 = new Random(), rand2 = new Random();

Instead of using two Randoms locally here, accept a single long as seed for a single Random. That would allow people to get the seed of the game and replay it, meaning the games are reproducible.


        while (m > 0) {
            int i = rand1.nextInt(this.N), j = rand2.nextInt(this.N);
            //System.out.println("i: " + i + "| j: " + j);
                           
            if (!tiles[i][j].hasMine()) {

The more mines you have in the game, the more likely you are approaching infinity with this when filling the field.

Most interestingly, randomly accessing a 2D grid without hitting the same tile twice is an interesting problem that I've been seeing for years. One solution is to push every index of every cell into an array, shuffle it, and use the top "mineCount" index to place the mines. That's called Fisher-Yates shuffle.

You can actually simplify this, by not only putting the cells into a 2D array for accessing, but by storing an additional reference in a List. Randomly shuffling the List of cells will yield the same result and the time used for placing the mines is always the same.


for (int[] nei : neighbors) {

Again, don't shorten variable names just because you can. It makes the code harder to read, harder to understand and harder to maintain.


                for (int[] nei : neighbors) {
                    
                    if (i + nei[0] < 0 || j + nei[1] < 0 || i + nei[0] >= this.N || j + nei[1] >= this.N) continue;
                    
                    if (!tiles[i+nei[0]][j+nei[1]].hasMine()) {
                        tiles[i+nei[0]][j+nei[1]].setMineCount(tiles[i+nei[0]][j+nei[1]].getMineCount()+1);
                    }
                }

It would be much easier if you calculate the count after you've placed all mines. Actually, it might be interesting to do this only at runtime when the player has clicked a cell.


        for (Tile[] tile : tiles) {
            
            for (Tile t : tile) {

I'd suggest to always use a int-loop to access the board, that keeps it rather easy to read and is always the same.


public void printBoard() {

Ideally, printing and interacting with the board (getting the user input) would be in another class.


System.out.println("Enter the grid size (less than or equal to 30): ");

Why the arbitrary limitation?


System.exit(0);

Calling System.exit is not a "global return", it's a "okay, let's kill the JVM" kinda thing. Threads will be stopped hard, finally blocks might not be executed and so on. It is a hard stop to all JVM activity and should only be used when you can afford to lose state.

Which, granted, is the case in this case, but still.


} while (!gameOver);

Inverting the condition would be easier to read.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.