package de.nisble.droidsweeper.game.replay;
import static de.nisble.droidsweeper.config.Constants.TIMER_PERIOD;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import de.nisble.droidsweeper.config.GameConfig;
import de.nisble.droidsweeper.game.Field;
import de.nisble.droidsweeper.game.Game;
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.utilities.LogDog;
import de.nisble.droidsweeper.utilities.Timer;
import de.nisble.droidsweeper.utilities.Timer.TimerObserver;
/** {@link Replay} player.<br>
* This class is used to show a game {@link Replay} that was loaded from the
* database or {@link Recorder recorded} right before. The key feature of this
* program to update the field widgets directly from native code makes showing a
* replay a bit tricky.<br>
* To update the {@link FieldStatus status} of the field widgets this
* class must know the widgets. Therefore this class holds a matrix of
* {@link FieldListener interfaces} to the field widgets.<br>
* It is mandatory for the view to distinguish between a real game and a replay.
* If its a game the field widgets must be registered in
* {@link Game#setFieldListener(FieldListener)} else in this class by calling
* {@link Player#setFieldListener(FieldListener)}.<br>
* The view should register a {@link PlayerObserver} in this class. After
* loading a {@link Replay} and while a call to {@link Player#play()} the
* interface method {@link PlayerObserver#onBuildGrid(GameConfig)} is invoked.
* Inside this method the view should create the game grid just as if the call
* comes from {@link Game}. But rather than registering the field widgets in the
* Game, the widgets should be registered here.
* @author Moritz Nisblé moritz.nisble@gmx.de
* @see {@link Recorder} for how to record a replay. */
public class Player implements TimerObserver {
private final String CLASSNAME = Player.class.getSimpleName();
private static final int STOPPED = 0;
private static final int PLAYING = 1;
private static final int PAUSED = 2;
private int mState = STOPPED;
private List<PlayerObserver> mObservers = new ArrayList<PlayerObserver>(1);
private Timer mTimer = new Timer();
private Replay mReplay = new Replay();
private ListIterator<TimeStep> mTsIter;
private FieldListener[][] mFlMatrix;
/** Instantiate and initialize a new replay player. */
public Player() {
mTimer.addListener(this);
}
/** Add an observer.
* @param l Observer. */
public void addObserver(PlayerObserver l) {
// Avoid multiple entries of the same object.
if (!mObservers.contains(l))
mObservers.add(l);
}
/** Remove an observer.
* @param l Observer. */
public void removeObserver(PlayerObserver l) {
mObservers.remove(l);
}
/** Register a field widget for the player to be able to update the status of
* the widgets.
* @param l A field widget that implements the {@link FieldListener}
* interface.
* @throws ArrayIndexOutOfBoundsException If a coordinate that the widget
* returns in its {@link FieldListener#getPosition()} method is
* out of the bounds of the current {@link GameConfig}. */
public void setFieldListener(FieldListener l) throws ArrayIndexOutOfBoundsException {
mFlMatrix[l.getPosition().X][l.getPosition().Y] = l;
}
/** Load a {@link Replay} by its game ID directly from the database.
* @param gameID The ID of the game in the database.
* @throws Exception Multiple exceptions are possible due to errors while
* loading from the database of deserialisation of the replay. */
public void load(long gameID) throws Exception {
// Stop the current replay
mState = STOPPED;
mTimer.stop();
mReplay = new Replay(DSDBAdapter.INSTANCE.getReplay(gameID));
mFlMatrix = new FieldListener[mReplay.getGameConfig().X][mReplay.getGameConfig().Y];
}
/** Load the given {@link Replay}.
* @param replay A Replay. */
public void load(Replay replay) {
// Stop the current replay
mState = STOPPED;
mTimer.stop();
// Make an internal copy
mReplay = new Replay(replay);
mFlMatrix = new FieldListener[mReplay.getGameConfig().X][mReplay.getGameConfig().Y];
}
/** Play a previously loaded {@link Replay}.<br>
* This invokes {@link PlayerObserver#onBuildGrid(GameConfig)} and passes
* the {@link GameConfig} of the Replay. The view should build the game grid
* and register the field widgets by passing them to
* {@link #setFieldListener(FieldListener)} */
public void play() {
if (!mReplay.getTimeSteps().isEmpty()) {
mState = PLAYING;
mTimer.stop();
mTsIter = mReplay.getTimeSteps().listIterator(0);
for (PlayerObserver l : mObservers) {
l.onTimeUpdate(0);
l.onRemainingBombsChanged(mReplay.getGameConfig().BOMBS);
/* Let the view build the game grid. */
l.onBuildGrid(mReplay.getGameConfig());
}
mTimer.start(TIMER_PERIOD);
}
}
/** Stop a {@link Replay}. */
public void stop() {
mState = STOPPED;
mTimer.stop();
}
/** Pause a currently running {@link Replay}. The Replay can be resumed by
* calling {@link #resume()}. */
public void pause() {
if (PLAYING == mState) {
mState = PAUSED;
mTimer.pause();
}
}
/** Resume a {@link #pause() paused} {@link Replay}. */
public void resume() {
if (PAUSED == mState) {
mState = PLAYING;
mTimer.resume();
}
}
@Override
public void onTick(long milliseconds) {
if (mTsIter.hasNext()) {
// Get next time step
TimeStep ts = mTsIter.next();
if (ts.TIME <= milliseconds) {
// LogDog.d(CLASSNAME, "HIT @ TimeStep " + ts.TIME + "ms <= " +
// milliseconds + "ms");
for (PlayerObserver l : mObservers) {
l.onRemainingBombsChanged(ts.BOMBS);
}
// Get field changes in this time step
for (Field f : ts.STEPS) {
// LogDog.v(CLASSNAME, "Changed field: X:" + f.POSITION.X +
// " Y:" + f.POSITION.Y + " ("
// + f.ADJACENT_BOMBS + ") to " + f.STATUS.toString());
try {
mFlMatrix[f.POSITION.X][f.POSITION.Y].onStatusChanged(f.STATUS, f.ADJACENT_BOMBS);
} catch (ArrayIndexOutOfBoundsException e) {
LogDog.e(CLASSNAME, "Out of bounds of FieldListener matrix @ X:" + f.POSITION.X + " Y:"
+ f.POSITION.Y, e);
}
}
} else {
mTsIter.previous();
}
} else {
mState = STOPPED;
mTimer.stop();
for (PlayerObserver l : mObservers) {
l.onReplayEnded();
}
}
}
@Override
public void onSecond(long seconds) {
for (PlayerObserver l : mObservers) {
l.onTimeUpdate(seconds * 1000);
}
}
/** @return True if a {@link Replay} is currently {@link #load(long) loaded}. */
public boolean isLoaded() {
return !mReplay.getTimeSteps().isEmpty();
}
/** @return True if the the {@link Player} is stopped. */
public boolean isStopped() {
return (STOPPED == mState);
}
/** @return True if the {@link Player} is currently playing. */
public boolean isPlaying() {
return PLAYING == mState;
}
/** @return True if the {@link Player} is paused. */
public boolean isPaused() {
return PAUSED == mState;
}
}