2
\$\begingroup\$

I have programmed a textual Minesweeper-Game in Java. For me it was much harder than I expected. I would be happy if you have tips how to improve the design!

The program can be tested here

Field.java

public class Field {
    private boolean explored;
    private boolean mined;
    private String modifiedAppearance;

    public Field(boolean mined) {
        explored = false;
        this.mined = mined;
        // This value can be used for displaying the field and is set by the
        // user of this class. If this value is not used by the user, the field
        // has a default appearance
        modifiedAppearance = null;
    }

    public void setAppearance(String appereance) {
        modifiedAppearance = appereance;
    }

    public String getAppearance() {
        if (modifiedAppearance == null) {
            if(explored) {
                if(mined) {
                    return "X";
                } else {
                    return "O";
                }
            } else {
                return "_";
            }
        } else {
            return modifiedAppearance;
        }
    }

    public boolean isExplored() {
        return explored;
    }

    public boolean isMined() {
        return mined;
    }

    public void explore() {
        this.explored = true;
    }
}

Pitch.java

import java.util.Random;
import java.util.Scanner;

public class Pitch {
    private int maxLength;
    private Field[][] fields;
    private int minedFields;
    private int fieldsAllreadyExplored;
    // if a mined field is discovered the pitch is destroyed
    private boolean destroyed;

    // maxLength determines width and height of the pitch (fields x fields)
    // fields are mined with a chance of 1 / minedPossibility 
    public Pitch(int maxLength, int minedPossibility) {
        // maxLength can not be higher than 99 and not lower than 10
        if (maxLength > 99) {
            maxLength = 99;
            System.out.println("size set to 99 (maximum)");
        } else if (maxLength < 10) {
            maxLength = 10;
            System.out.println("size set to 10 (minimum)");
        }
        this.maxLength = maxLength;
        // generate the pitch
        fields = new Field[maxLength][maxLength];
        Random random = new Random();
        for (int i = 0; i < maxLength; i++) {
            for (int j = 0; j < maxLength; j++) {
                if (random.nextInt(minedPossibility) + 1 == minedPossibility) {
                    fields[i][j] = new Field(true);
                    minedFields++;
                } else {
                    fields[i][j] = new Field(false);
                }
            }
        }
    }

    public boolean isCompletelyExplored() {
        return maxLength * maxLength - minedFields - fieldsAllreadyExplored
        == 0;
    }

    public boolean isDestroyed() {
        return destroyed;
    }

    // tells the user how to enter data
    public void displayUserInformation() {
        System.out.println("x: line number");
        System.out.println("y: column number\n");
    }

    // returns true if discovered field is mined
    public boolean explore(int x, int y) {
        fieldsAllreadyExplored++;
        fields[x][y].explore();

        if (fields[x][y].isMined()) {
            destroyed = true;
            return true;
        }

        int[][] neighborFields = getNearbyFields(x, y);

        // check how many neighbor fields are mined
        int numberOfMinedNeighborFields = 0;
        for (int i = 0; i < neighborFields.length; i++) {
            if (fields[neighborFields[i][0]][neighborFields[i][1]].isMined()) {
                numberOfMinedNeighborFields++;
            }
        }

        // set appearance and if no neighbor fields are mined discover them too
        if (numberOfMinedNeighborFields == 0) {
            fields[x][y].setAppearance("0");

            // discover neighbor fields
            for(int i = 0; i < neighborFields.length; i++) {
                if (!fields[neighborFields[i][0]][neighborFields[i][1]]
                .isExplored()) {
                    explore(neighborFields[i][0], neighborFields[i][1]);
                }
            }
        } else {
            fields[x][y].setAppearance(String.valueOf(
            numberOfMinedNeighborFields));
            System.out.print("");
        }

        return false;
    }

    private int[][] getNearbyFields(int x, int y) {
        // determine the coordinates of neighbor fields
        int[][] allCoordinates = new int [][] { {x-1, y-1}, {x-1, y}, 
        {x-1, y+1}, {x, y-1}, {x, y+1}, {x+1, y-1}, {x+1, y}, {x+1, y+1} };

        // check how many valid coordinates exist
        int numberOfValidCoordinates = 0;
        int[] indices = new int[8];
        int indicesIndex = 0; // i find it kind of funny
        for (int i = 0; i < 8; i++) {
            if((allCoordinates[i][0] > -1 && allCoordinates[i][0] < 
            maxLength) && (allCoordinates[i][1] > -1 && allCoordinates[i][1] < 
            maxLength)) {
                numberOfValidCoordinates++;
                indices[indicesIndex] = i;
                indicesIndex++;
            }
        }

        // add the valid coordinates to a list and return it
        int[][] validCoordinates = new int[numberOfValidCoordinates][2];
        for(int i = 0; i < numberOfValidCoordinates; i++) {
            validCoordinates[i] = allCoordinates[indices[i]];
        }
        return validCoordinates;
    }

    // asks the user which field he wants to explore
    public void getUserInput(Scanner scanner) {
        System.out.print("x: ");
        int x = Integer.parseInt(scanner.next());
        System.out.print("y: ");
        int y = Integer.parseInt(scanner.next());
        System.out.println();
        explore(x, y);
    }

    // displays all fields of the pitch with the indication of the coordinates
    public void display() {
        // display x coordinates
        System.out.print("     ");
        for (int i = 0; i < maxLength; i++) {
            if (i < 10) {
                System.out.print(i + "  ");
            } else {
                System.out.print(i + " ");
            }
        }
        System.out.println("\n\n");

        // display all lines
        for (int i = 0; i < maxLength; i++) {
            if (i < 10) {
                System.out.print(i + "    ");
            } else {
                System.out.print(i + "   ");
            }

            for(int j = 0; j < maxLength; j++) {
                System.out.print(fields[i][j].getAppearance() + "  ");
            }
            System.out.println("\n");
        }
    }
}

Main.java

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Pitch pitch = new Pitch(10, 5);
        pitch.displayUserInformation();
        while (!pitch.isCompletelyExplored() && !pitch.isDestroyed()) {
            pitch.display();
            pitch.getUserInput(scanner);
        }

        // game over
        pitch.display();
        if(pitch.isCompletelyExplored()) {
            System.out.println("You have won the game!");
        } else {
            System.out.println("You stepped on a mine");
        }
        scanner.close();
    }
}
\$\endgroup\$
  • \$\begingroup\$ How does the player mark/unmark mines? \$\endgroup\$ – 200_success Feb 26 '18 at 20:48
  • \$\begingroup\$ Unfortunately this feature is not implemented. \$\endgroup\$ – Dexter Thorn Feb 26 '18 at 20:49
  • \$\begingroup\$ How do you determine the number of bombs? I had 14? \$\endgroup\$ – Raystafarian Feb 26 '18 at 21:48
  • \$\begingroup\$ How many bombs are in the pitch is decided randomly. About every eighth field is mined. In the main-method the pitch gets generated. There you can change the value. \$\endgroup\$ – Dexter Thorn Feb 26 '18 at 21:53
  • \$\begingroup\$ Ah public Pitch(int maxLength, int minedPossibility)? \$\endgroup\$ – Raystafarian Feb 26 '18 at 21:57
3
\$\begingroup\$

Some tips I think I can give about each class:

Main.java

Some minor remarks:

Field.java

Again some minor remarks

  • You could remove the initialization in the constructor of explored and modifiedAppearance, because the default values of boolean and a reference type are false and null
  • I would consider renaming mined, to me it looks like it means if a field has been explored or not, instead of containing a mine. Perhaps hasMine/containsMine?
  • The getAppearance method also seems a bit wonky, say you accidently set the appearance from the outside as a number, on a field with a mine. It will create some problems :D

Pitch.java

  • You added documentation above the constructor which is nice. You also added a comment on the first line about the constraints imposed on the constructor parameters, which is also nice. If you would now convert these to JavaDoc, users of the Pitch class could also benefit from this!
  • Also in the constructor, you added a comment generate the pitch. Why not extract this block of code below it to a method :) e.g. fields = generatePitch(maxLength, minedPossibility);
  • For the explore method, should you still allow all the things being executed when the pitch has been destroyed? (I know you exit from the outside, but say the Pitch class will be used somewhere else later on)
  • About the same method, you seem to have a bug. Should I keep on mining the same field over and over, I will win the game nonetheless. The fieldsAlreadyExplored is always incremented.
  • In the getNearbyFields method, you talk about a list of coordinates. Why not make it one? Add or reuse a class Point or Coordinate. Then create a real list, e.g. List<Coordinate>. With this, you could even use some Java 8 additions:

    List<Coordinate> validCoordinates = coordinates.stream()
        .filter(coordinate -> coordinate.x() > -1 && coordinate.x() < maxLength)
        .filter(coordinate -> coordinate.y() > -1 && coordinate.y() < maxLength)
        .collect(toList())
    

    or

    coordinates.removeIf(coordinate -> coordinate.x() < 0 ...);
    
  • Finally, it is not really the responsibility of the Pitch class to handle user input, nor handle the displaying of itself. I'd take it out and move it to an other dedicated class.

\$\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.