/* * Copyright 2012, 2013 Evan Flynn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.foobar.minesweeper.model; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkElementIndex; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.concurrent.CopyOnWriteArrayList; /** * Provides a model for a Minesweeper game. Objects that wish to be notified * with updates from this class can call {@code addFieldHandler}. * * This class is not thread-safe. * * @author Evan Flynn */ public final class Minefield { private final int columns; private final int rows; private final int mines; private int unrevealed; private State state; private final Square[][] table; private final List<FieldHandler> handlers = new CopyOnWriteArrayList<>(); private final List<Square> mineSet = new ArrayList<>(); private final Random random; /** * Creates a {@code Minefield}. * * @param rows the number of rows in the {@code Minefield} * @param columns the number of columns in the {@code Minefield} * @param mines the number of mines in the {@code Minefield} * @throws IllegalArgumentException if {@code rows}, {@code columns}, or * {@code mines} is negative. */ public Minefield(int rows, int columns, int mines) { this(rows, columns, mines, new Random()); } Minefield(int rows, int columns, int mines, Random random) { // FIXME: only does basic checks for sanity. checkArgument(rows > 0, "rows must be positive: %s", rows); checkArgument(columns > 0, "columns must be positive: %s", columns); checkArgument(mines > 0, "mines must be positive: %s", mines); this.rows = rows; this.columns = columns; this.mines = mines; this.random = random; table = new Square[rows][columns]; reset(); } /** * Adds a handler for Minefield events. * * @param handler the field handler * @return {@code HandlerRegistration} used to remove this handler */ public HandlerRegistration addFieldHandler(final FieldHandler handler) { handlers.add(handler); updateBoard(); return () -> handlers.remove(handler); } /** * Gets the number of columns in the minefield. * * @return the number of columns in the minefield. */ public int getColumnCount() { return columns; } /** * Gets the current game state. * * @return the current game state. */ public State getState() { return state; } /** * Gets number of mines * * @return the mines */ public int getMines() { return mines; } /** * Gets number of rows * * @return number of rows */ public int getRowCount() { return rows; } /** * Gets the square at {@code row} and {@code column}. * * @param row row to find {@code Square} with * @param column column to find {@code Square} with * @throws IndexOutOfBoundsException if {@code row} is negative or greater * than or equal to {@code getRowCount()} * @throws IndexOutOfBoundsException if {@code column} is negative or greater * than or equal to {@code getColumnCount()} * @return square at {@code row} and {@code column} */ public Square getSquare(int row, int column) { checkElementIndex(row, rows); checkElementIndex(column, columns); return table[row][column]; } /** * Is the game over? * * @return whether the game is over */ public boolean isGameOver() { return (state == State.LOST || state == State.WON); } /** * Resets the Minesweeper game. */ public void reset() { mineSet.clear(); unrevealed = (rows * columns) - mines; for (int r = 0; r < rows; r++) { for (int c = 0; c < columns; c++) { table[r][c] = new Square(this, r, c); } } updateBoard(); setState(State.START); } void updateSquare(Square square) { for (FieldHandler handler : handlers) { handler.updateSquare(square); } } void reveal(Square square) { assert !isGameOver() && square.getType() == Squares.BLANK; if (state == State.START) { firstClick(square); } cascade(square); } void onGameLost() { for (Square[] columns : table) { for (Square square : columns) { square.onGameLost(); } } updateBoard(); setState(State.LOST); } List<Square> findNeighbors(Square square) { List<Square> neighbors = new ArrayList<>(8); int row = square.getRow(); int column = square.getColumn(); for (int r = row - 1; r <= row + 1; r++) { for (int c = column - 1; c <= column + 1; c++) { if ((r != row || c != column) && r >= 0 && c >= 0 && r < rows && c < columns) { neighbors.add(table[r][c]); } } } return neighbors; } void updateBoard() { handlers.forEach(FieldHandler::updateBoard); } private void cascade(Square start) { int exposed = start.visit(); unrevealed -= exposed; if (unrevealed == 0) { mineSet.forEach(Square::onGameWon); setState(State.WON); updateBoard(); } else if (exposed == 1) { updateSquare(start); } else { updateBoard(); } } private void firstClick(Square first) { setState(State.PLAYING); List<Square> flat = new ArrayList<>(rows * columns); for(int i = 0; i < rows; i++) { for(int j = 0; j < columns; j++) { if (table[i][j] != first) { flat.add(table[i][j]); } } } Collections.shuffle(flat, random); mineSet.addAll(flat.subList(0, mines)); for(Square square : mineSet) { square.setMine(true); findNeighbors(square).forEach(Square::addNearbyMine); } } private void setState(State state) { if (this.state != state) { this.state = state; for (FieldHandler handler : handlers) { handler.changeState(state); } } } /** * Handler for {@code Minefield} events. */ public interface FieldHandler { /** * Called when a {@code Square} was changed. * * @param square square to update. */ void updateSquare(Square square); /** * Called when the entire board was changed. This occurs on a reset * or a cascade. */ void updateBoard(); /** * Called when the game state was updated. */ void changeState(State state); } /** * The current state of the game. * * The initial state is {@code START}. It changes to {@code PLAYING} when the * first square has been revealed. */ public enum State { /** Indicates that a mine was tripped and the game is over. */ LOST, /** The game is in progress. */ PLAYING, /** The game was reset. */ START, /** The game has been won. */ WON } }