As I'm working with a proprietary programming language in my daily job, my java skills unfortunately missed out. In order to change this I am currently working on a minesweeper clone. So far, I have finished the basic implementation (business logic and command line view). The next step will be a graphical user interface.
Now I am looking for tips from experienced java developers in order to further improve the code and myself.
- How can I make the code more readable and simplistic?
- Are there any code smells?
- Can I use more design pattern to get more flexibility?
I would appreciate your comments and any kind of positive and negative feedback.
The full and up-to-date code is published on GitHub.
Here are some original code extracts:
Cell.java
public class Cell implements CellRO {
private boolean isVisited;
private boolean isMine;
private boolean markedAsBomb;
private int countOfNeighbourMines;
public int getCountOfNeighbourMines() {
return countOfNeighbourMines;
}
public void setCountOfNeighbourMines(final int count) {
countOfNeighbourMines = count;
}
public Cell() {
isVisited = false;
isMine = false;
markedAsBomb = false;
countOfNeighbourMines = 0;
}
public boolean isVisited() {
return isVisited;
}
public boolean isMine() {
return isMine;
}
public boolean isMarkedAsBomb() {
return markedAsBomb;
}
public void visit() {
if (isVisited) {
throw new IllegalStateException("A cell can only be visited once!");
}
isVisited = true;
}
public void setMine() {
isMine = true;
}
public void changeMarkedAsBomb() {
markedAsBomb = !markedAsBomb;
}
public void removeMine() {
isMine = false;
}
}
Cellbuilder.java
import model.cell.Cell;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
public final class CellBuilder {
private final int countOfRows;
private final int countOfColumns;
private final int countOfMines;
public CellBuilder(final int rowCount, final int colCount, final int minesCount) {
countOfRows = rowCount;
countOfColumns = colCount;
countOfMines = Math.max(0, Math.min(minesCount, countOfRows * countOfColumns));
}
public Cell[][] buildBoard() {
Cell[][] board = new Cell[this.countOfRows][this.countOfColumns];
populateBoardWithCells(board);
populateBoardWithMines(board);
return board;
}
private void populateBoardWithCells(final Cell[][] board) {
for (int row = 0; row < this.countOfRows; row++) {
for (int col = 0; col < this.countOfColumns; col++) {
board[row][col] = new Cell();
}
}
}
private void populateBoardWithMines(final Cell[][] board) {
int mines = 0;
for (int row = 0; row < this.countOfRows; row++) {
for (int col = 0; col < this.countOfColumns; col++) {
if (mines < countOfMines) {
board[row][col].setMine();
mines++;
}
}
}
shuffleBoard(board);
}
private void shuffleBoard(final Cell[][] board) {
// Fisher–Yates algorithm
Random ran = new Random();
for (int row = board.length - 1; row > 0; row--) {
for (int col = board[row].length - 1; col > 0; col--) {
int rowRandom = ran.nextInt(row + 1);
int colRandom = ran.nextInt(col + 1);
Cell temp = board[row][col];
board[row][col] = board[rowRandom][colRandom];
board[rowRandom][colRandom] = temp;
}
}
}
public int getCountOfMines() {
return this.countOfMines;
}
}
Board.java
import model.cell.Cell;
import model.cell.NullCell;
import util.GameDifficulty;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public final class Board {
public static final int MINIMUM_ROWS = 4;
public static final int MINIMUM_COLUMNS = 4;
private final Cell[][] field;
private final int rowCount;
private final int colCount;
private final int countOfMines;
/*
Default constructor will use default values
*/
public Board() {
this(MINIMUM_ROWS, MINIMUM_COLUMNS, GameDifficulty.EASY);
}
public Board(final int countOfRows, final int countOfColumns,
final GameDifficulty difficulty) {
rowCount = Math.max(countOfRows, MINIMUM_ROWS);
colCount = Math.max(countOfColumns, MINIMUM_COLUMNS);
countOfMines = getCountOfMinesByDifficulty(difficulty);
CellBuilder builder = new CellBuilder(rowCount, colCount, countOfMines);
field = builder.buildBoard();
setNeighbourCount();
}
public Board(final Cell[][] fieldOfCells, final int mineCount) {
this.rowCount = fieldOfCells.length;
this.colCount = fieldOfCells[0].length;
this.countOfMines = mineCount;
this.field = fieldOfCells;
}
private int getCountOfMinesByDifficulty(final GameDifficulty difficulty) {
return (int) (difficulty.getPercentage() * getCountOfCells());
}
public Cell getCell(final int row, final int col) {
if (!isValidPosition(row, col)) {
return NullCell.getInstance();
}
return field[row][col];
}
private void setNeighbourCount() {
for (int row = 0; row < rowCount; row++) {
for (int col = 0; col < colCount; col++) {
Cell cell = getCell(row, col);
cell.setCountOfNeighbourMines(getCountOfNeighbourMines(cell));
}
}
}
private boolean isValidPosition(final int row, final int col) {
if (row < 0 || row >= rowCount) {
return false;
}
if (col < 0 || col >= colCount) {
return false;
}
return field[row][col] != null;
}
public int getRowCount() {
return this.rowCount;
}
public int getColCount() {
return this.colCount;
}
public List<Cell> getNeighbourCells(final Cell cell) {
for (int row = 0; row < rowCount; row++) {
for (int col = 0; col < colCount; col++) {
if (getCell(row, col) == cell) {
return getNeighbourCells(row, col);
}
}
}
return Collections.emptyList();
}
private List<Cell> getNeighbourCells(final int row, final int col) {
List<Cell> neighbours = new ArrayList<>();
if (isValidPosition(row - 1, col - 1)) {
neighbours.add(field[row - 1][col - 1]);
}
if (isValidPosition(row - 1, col)) {
neighbours.add(field[row - 1][col]);
}
if (isValidPosition(row - 1, col + 1)) {
neighbours.add(field[row - 1][col + 1]);
}
if (isValidPosition(row, col - 1)) {
neighbours.add(field[row][col - 1]);
}
if (isValidPosition(row, col + 1)) {
neighbours.add(field[row][col + 1]);
}
if (isValidPosition(row + 1, col - 1)) {
neighbours.add(field[row + 1][col - 1]);
}
if (isValidPosition(row + 1, col)) {
neighbours.add(field[row + 1][col]);
}
if (isValidPosition(row + 1, col + 1)) {
neighbours.add(field[row + 1][col + 1]);
}
return neighbours;
}
public int getCountOfNeighbourMines(final Cell cell) {
int countOfNeighbourMines = 0;
List<Cell> neighbours = getNeighbourCells(cell);
for (Cell neighbour : neighbours) {
if (neighbour.isMine()) {
countOfNeighbourMines++;
}
}
return countOfNeighbourMines;
}
public void moveMineToRandomCell(final int row, final int col) {
Cell cell = getCell(row, col);
if (!cell.isMine()) {
return;
}
int rowNew = row;
int colNew = col;
Random ran = new Random();
while (rowNew == row || colNew == col || getCell(rowNew, colNew).isMine()) {
rowNew = ran.nextInt(rowCount);
colNew = ran.nextInt(colCount);
}
cell.removeMine();
getCell(rowNew, colNew).setMine();
}
public int getCountOfMines() {
return countOfMines;
}
private int getCountOfCells() {
return rowCount * colCount;
}
}
GameStateRunning.java
import model.GameModelImpl;
import model.cell.Cell;
import java.util.List;
public class GameStateRunning implements GameModelState {
@Override
public void openCell(final GameModelImpl context, final int row, final int col) {
Cell cell = context.getCell(row, col);
if (cell.isMine()) {
context.setState(new GameStateLost());
return;
}
if (cell.isVisited() || context.getBoard().getCountOfNeighbourMines(cell) == 0) {
openCellR(context, cell);
} else {
cell.visit();
}
if (gameIsWon(context)) {
context.setState(new GameStateWon());
}
}
@Override
public void changeMarkedAsBomb(final GameModelImpl context, final int row, final int col) {
Cell cell = context.getCell(row, col);
cell.changeMarkedAsBomb();
}
@Override
public void visitAllAndRemoveMarks(final GameModelImpl context) {
// Operation not allowed in this state!
}
private void openCellR(final GameModelImpl context, final Cell cell) {
List<Cell> neighbours = context.getBoard().getNeighbourCells(cell);
for (Cell neighbour : neighbours) {
if (neighbour.isVisited()) {
continue;
}
if (cell.isMine() && !cell.isMarkedAsBomb()) {
context.setState(new GameStateLost());
return;
} else {
neighbour.visit();
if (context.getBoard().getCountOfNeighbourMines(neighbour) == 0) {
openCellR(context, neighbour);
}
}
}
}
private boolean gameIsWon(final GameModelImpl context) {
for (int row = 0; row < context.getBoard().getRowCount(); row++) {
for (int col = 0; col < context.getBoard().getColCount(); col++) {
if (!context.getBoard().getCell(row, col).isMine() && !context.getBoard().getCell(row, col).isVisited()) {
return false;
}
}
}
return true;
}
}