package de.nisble.droidsweeper.game.replay; import java.util.ArrayList; import java.util.List; import de.nisble.droidsweeper.config.GameConfig; import de.nisble.droidsweeper.game.Field; import de.nisble.droidsweeper.game.Position; 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.utilities.LogDog; /** {@link Replay} recorder.<br> * This class abstracts the recording of games. Because only real changes of * fields should be recorded, the procedure of recording a game needs a little * explanation.<br> * <ol> * <li>An instance of this class should be registered as observer to * {@link MineSweeperMatrix} by passing it to * {@link MineSweeperMatrix#addMatrixObserver(MatrixObserver)}.</li> * <li>A new recording is started by calling {@link #newRecord(GameConfig)}.</li> * <li>When the player now clicks a field this class receives events over * changings in the of the game via the {@link MatrixObserver} interface. Each * click may cause multiple {@link Field fields} to change its state. So multiple * events are received and multiple changes are recorded for a single click (see * {@link Replay} for an explanation of the structure of a replay). Because for * this class its impossible to know which event is the last one for a click * (i.e. a single {@link TimeStep}) the help of the controlling instance is * needed.</li> * <li>So after a click is finished (all events are received) the controlling * instance must call {@link #finalizeStep(long)} to finish the record step. The * events are than stored inside a {@link Replay}.</li> * <li>After the game is finished and the last click is finished the * {@link Replay} can be requested by calling {@link #getReplay()}.</li> * </ol> * @author Moritz Nisblé moritz.nisble@gmx.de * @see {@link Player} for how to replay a recorded {@link Replay}. */ public class Recorder implements MatrixObserver { private final String CLASSNAME = Recorder.class.getSimpleName(); private boolean mRecordFlag = false; private Replay mCurrent = new Replay(); private Replay mComplete = new Replay(); private List<Field> mStepBuffer = new ArrayList<Field>(); private GameStatus mGameStatus = GameStatus.READY; private int mRemainingBombs = 0; /** Start a new record. * @param c The {@link GameConfig configuration}. */ public void newRecord(GameConfig c) { mRecordFlag = true; mCurrent = new Replay(); mCurrent.setGameConfig(c); mCurrent.setEpochTime(System.currentTimeMillis()); /* Reset internal state */ mStepBuffer.clear(); mGameStatus = GameStatus.READY; mRemainingBombs = c.BOMBS; } /** Finalize a step. * @param milliseconds The current play time in milliseconds. */ public void finalizeStep(long milliseconds) { finalizeStep(milliseconds, mGameStatus, mRemainingBombs); } /** Finalize step.<br> * <b>Note:</b> Probably better use {@link #finalizeStep(long)}. This * version is normally not needed since the gameStatus and * remainingBombs for the current step are already known because passed to * {@link MatrixObserver} by the game logic. This version overwrites that * information. * @param milliseconds The current play time in milliseconds. * @param gameStatus The current {@link GameStatus}. * @param remainingBombs The count of remaining bombs on the game grid. */ public void finalizeStep(long milliseconds, GameStatus gameStatus, int remainingBombs) { if (mRecordFlag) { TimeStep s = new TimeStep(milliseconds, gameStatus.ordinal(), remainingBombs, mStepBuffer.toArray(new Field[mStepBuffer.size()])); // LogDog.i(CLASSNAME, "SNAPSHOT @ " + s.TIME + "ms GameStatus: " // + GameStatus.fromInt(s.GAMESTATUS).toString() + // " RemainingBombs: " + s.BOMBS + " Fields changed: " // + s.STEPS.length); // for (Field f : s.STEPS) { // LogDog.i(CLASSNAME, "\t" + f.toString()); // } mCurrent.addTimeStep(s); mStepBuffer.clear(); if (GameStatus.WON == gameStatus || GameStatus.LOST == gameStatus) { mCurrent.setPlayTime(milliseconds); mRecordFlag = false; mComplete = new Replay(mCurrent); } } else { LogDog.w(CLASSNAME, "Not recording"); } } /** Get a deep copy of the last completed {@link Replay}.<br> * Note that its not possible to get the replay while currently recording. * So the replay may be empty when the first game is running when you call * this method. * @return A copy of the last completed {@link Replay}. */ public Replay getReplay() { return new Replay(mComplete); } @Override public void onGameStatusChanged(GameStatus newStatus) { /* Buffer the status until someone finalizes the current step */ mGameStatus = newStatus; } @Override public void onRemainingBombsChanged(int remainingBombs) { /* Buffer the bomb count until someone finalizes the current step */ mRemainingBombs = remainingBombs; } @Override public void afterFieldStatusChanged(Position p, FieldStatus fs, int adjacentBombs) { /* Buffer field changes until someone finalizes the current step */ if (mRecordFlag) mStepBuffer.add(new Field(p, fs, adjacentBombs)); } /** @return True when currently recording. */ public boolean isRecording() { return mRecordFlag; } }