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();
}
}
1 Answer 1
Some tips I think I can give about each class:
Main.java
Some minor remarks:
- You could think of adding some way to ask for a user to input the Pitch properties
- You can close the scanner using
try-with-resources
: see try-with-resources documentation
Field.java
Again some minor remarks
- You could remove the initialization in the constructor of
explored
andmodifiedAppearance
, because the default values ofboolean
and a reference type arefalse
andnull
- 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. PerhapshasMine/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 classPoint
orCoordinate
. 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.
public Pitch(int maxLength, int minedPossibility)
? \$\endgroup\$