package de.nisble.droidsweeper.game;
import java.util.ArrayList;
import java.util.List;
import de.nisble.droidsweeper.config.GameConfig;
import de.nisble.droidsweeper.config.Level;
import de.nisble.droidsweeper.game.database.DSDBAdapter;
import de.nisble.droidsweeper.game.jni.FieldListener;
import de.nisble.droidsweeper.game.jni.FieldStatus;
import de.nisble.droidsweeper.game.jni.GameStatus;
import de.nisble.droidsweeper.game.jni.MatrixObserver;
import de.nisble.droidsweeper.game.jni.MineSweeperMatrix;
import de.nisble.droidsweeper.game.replay.Recorder;
import de.nisble.droidsweeper.game.replay.Replay;
import de.nisble.droidsweeper.utilities.Timer;
import de.nisble.droidsweeper.utilities.LogDog;
import de.nisble.droidsweeper.utilities.Timer.TimerObserver;
import static de.nisble.droidsweeper.config.Constants.*;
/** This class coordinates a game.<br>
* It completely abstracts the game procedure and has no dependencies to the
* view. View classes should implement the {@link GameObserver observer
* interface} to get updates on important changes of the internal state of the
* game. Architectural this class sits between the view and the
* {@link MineSweeperMatrix interface to the native library libmsm}. It maintains
* a timer that counts the elapsed playtime and records each game. A view class
* is responsible for loading and showing a replay.
* <ul>
* <li>Singleton: Use the public final INSTANCE member.</li>
* </ul>
* @author Moritz Nisblé moritz.nisble@gmx.de */
public class Game implements MatrixObserver, TimerObserver {
private final String CLASSNAME = Game.class.getSimpleName();
/** The instance. */
public static final Game INSTANCE = new Game();
private GameConfig mConfig = new GameConfig(Level.EASY);
private Timer mTimer = new Timer();
private Recorder mRecorder = new Recorder();
private List<GameObserver> mObservers = new ArrayList<GameObserver>(2);
private Game() {
MineSweeperMatrix.INSTANCE.addMatrixObserver(this);
// Add recorder as MatrixObserver
MineSweeperMatrix.INSTANCE.addMatrixObserver(mRecorder);
mTimer.addListener(this);
}
/** Add an observer.
* @param l Observer. */
public void addObserver(GameObserver l) {
// Avoid multiple entries of the same object.
if (!mObservers.contains(l))
mObservers.add(l);
}
/** Remove an observer.
* @param l Observer. */
public void removeObserver(GameObserver l) {
mObservers.remove(l);
}
/** @return The currently loaded {@link GameConfig}. */
public GameConfig getGameConfig() {
return mConfig;
}
/** This function only return true when a new game was created but not
* started (i.e. no click was made).
* @return True if the game is in {@link GameStatus#RUNNING} state. */
@Deprecated
public boolean isOrientationChangeable() {
return (MineSweeperMatrix.INSTANCE.isReady());
}
/** Register a FieldListener.
* @note This connects a FieldListener directly to a field object
* in the native libmsm.
* @param l Typically a widget that is able to represent a single field.
* @throws Exception An IndexOutOfBoundsException when the coordinates
* that the given FieldListener returns from its getPosition()
* method is out of the bounds of the matrix in the GameConfig
* passed to start() before. */
public void setFieldListener(FieldListener l) throws Exception {
MineSweeperMatrix.INSTANCE.setFieldListener(l);
}
/* A click on the grid is only acceptable after initialization
* of a new game or while running. */
private boolean isClickAcceptable(GameStatus gs) {
return (gs != GameStatus.LOST && gs != GameStatus.WON);
}
/* Actions to perform before control is passed to libmsm. */
private void beforeClick() {
if (!mTimer.isRunning()) {
mTimer.start(TIMER_PERIOD);
}
}
/* Actions to perform after control is passed back and all actions
* for a click are performed by libmsm. The old status is used to
* detect changes of the internal status. */
private void afterClick(GameStatus old, Position p) {
GameStatus newStatus = MineSweeperMatrix.INSTANCE.gameStatus();
mRecorder.finalizeStep(mTimer.getMilliseconds());
if (newStatus != old) {
// LogDog.i(CLASSNAME, "GAMESTATUS changed to " +
// newStatus.toString());
switch (newStatus) {
case RUNNING:
break;
case LOST:
mTimer.stop();
for (GameObserver l : mObservers) {
if (l.onLost(mTimer.getMilliseconds())) {
revealAll();
}
}
break;
case READY:
break;
case WON:
mTimer.stop();
boolean highscore = false;
try {
if (mConfig.LEVEL != Level.CUSTOM) {
highscore = DSDBAdapter.INSTANCE.isHighScore(mConfig.LEVEL, mTimer.getMilliseconds());
}
// LogDog.i(CLASSNAME, "Highscore: " + highscore);
} catch (Exception e) {
LogDog.e(CLASSNAME, e.getMessage(), e);
}
for (GameObserver l : mObservers) {
l.onWon(mTimer.getMilliseconds(), highscore);
}
break;
default:
break;
}
}
}
/** Reveal a field fields.
* @param p The position. */
public void revealField(Position p) {
GameStatus oldStatus = MineSweeperMatrix.INSTANCE.gameStatus();
if (isClickAcceptable(oldStatus)) {
beforeClick();
MineSweeperMatrix.INSTANCE.reveal(p);
afterClick(oldStatus, p);
}
}
/** Reveal all fields.<br>
* <b>This may fire {@link GameObserver} events like
* {@link GameObserver#onLost(long)}.</b> */
public void revealAll() {
for (int y = 0; y < mConfig.Y; ++y) {
for (int x = 0; x < mConfig.X; ++x) {
MineSweeperMatrix.INSTANCE.reveal(new Position(x, y));
}
}
}
/** Cycle through marks of fields.<br>
* <b>Cycle sequence:</b> {@link FieldStatus#HIDDEN HIDDEN},
* {@link FieldStatus#MARKED MARKED} , {@link FieldStatus#QUERIED QUERIED}<br>
* <b>Note:</b> Its not possible to {@link #revealField(Position)
* reveal} a field with the status {@link FieldStatus#MARKED MARKED} while
* its possible in both other states. This is prevented by the native
* library.
* @param p The position. */
public void cycleMark(Position p) {
GameStatus oldStatus = MineSweeperMatrix.INSTANCE.gameStatus();
if (isClickAcceptable(oldStatus)) {
beforeClick();
MineSweeperMatrix.INSTANCE.cycleMark(p);
afterClick(oldStatus, p);
}
}
/** Start a new game with old {@link GameConfig}.
* @see #start(GameConfig) */
public void start() {
start(mConfig);
}
/** Start a new game with the given CameConfig.<br>
* <b>Note:</b> There are some things the calling view class should take
* into account when calling this method. After resetting the internal state
* of this class (timer and replay recorder) and instructing the native
* library to create a new game matrix,
* {@link GameObserver#onBuildGrid(GameConfig)} is called on each observer.
* The view should create the game grid from inside that callback. Each
* field widget that is created in that process should be registered by a
* call to {@link #setFieldListener(FieldListener)} for the native library
* to ba able to inform the widget over changes in its state.
* @param c A {@link GameConfig}. */
public void start(GameConfig c) {
// Make an internal copy of the GameConfig.
mConfig = c;
mTimer.stop();
mRecorder.newRecord(c);
// Create a new matrix
MineSweeperMatrix.INSTANCE.create(mConfig);
// Update observers
for (GameObserver l : mObservers) {
l.onBuildGrid(mConfig);
l.onTimeUpdate(0);
l.onRemainingBombsChanged(mConfig.BOMBS);
}
}
/** Stop the current game. */
public void stop() {
mTimer.stop();
}
/** Pause the current game.<br>
* It can be resumed later by calling {@link #resume()}. */
public void pause() {
if (MineSweeperMatrix.INSTANCE.isRunning() && mTimer.isRunning()) {
mTimer.pause();
}
}
/** Try to resume a running game or a replay.
* @return True if there is a game or replay to resume, else false. */
public boolean resume() {
if (MineSweeperMatrix.INSTANCE.isRunning() && mTimer.isPaused()) {
mTimer.resume();
return true;
}
return false;
}
/** @return Get a copy of the {@link Replay} of the last completed game. */
public Replay getReplay() {
return mRecorder.getReplay();
}
@Override
public void onGameStatusChanged(GameStatus newStatus) {
/* Don't use this callback!
* Makes things more complicated and can cause a high call stack! */
}
@Override
public void onRemainingBombsChanged(int remainingBombs) {
for (GameObserver l : mObservers) {
l.onRemainingBombsChanged(remainingBombs);
}
}
@Override
public void afterFieldStatusChanged(Position p, FieldStatus fs, int adjacentBombs) {
}
@Override
public void onTick(long milliseconds) {
}
@Override
public void onSecond(long seconds) {
for (GameObserver l : mObservers) {
l.onTimeUpdate(seconds * 1000);
}
}
}