/* * Copyright 2015 Daniel Dittmar * * 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 dan.dit.whatsthat.riddle.control; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.view.MotionEvent; import com.plattysoft.leonids.ParticleSystem; import dan.dit.whatsthat.BuildConfig; import dan.dit.whatsthat.achievement.AchievementData; import dan.dit.whatsthat.achievement.AchievementProperties; import dan.dit.whatsthat.image.Image; import dan.dit.whatsthat.preferences.Language; import dan.dit.whatsthat.riddle.Riddle; import dan.dit.whatsthat.riddle.RiddleConfig; import dan.dit.whatsthat.riddle.RiddleView; import dan.dit.whatsthat.riddle.achievement.AchievementDataRiddleGame; import dan.dit.whatsthat.riddle.achievement.AchievementDataRiddleType; import dan.dit.whatsthat.riddle.achievement.GameAchievement; import dan.dit.whatsthat.riddle.types.TypesHolder; import dan.dit.whatsthat.solution.Solution; import dan.dit.whatsthat.solution.SolutionInput; import dan.dit.whatsthat.solution.SolutionInputListener; import dan.dit.whatsthat.solution.SolutionInputManager; import dan.dit.whatsthat.solution.SolutionInputView; import dan.dit.whatsthat.util.general.PercentProgressListener; import dan.dit.whatsthat.util.compaction.CompactedDataCorruptException; import dan.dit.whatsthat.util.compaction.Compacter; import dan.dit.whatsthat.util.image.Dimension; import dan.dit.whatsthat.util.image.ImageUtil; /** * The "important" base type for the things that can be actually played! Decorates a Riddle object * and makes it playable, keeping the required data like bitmaps and precalculations that can take a heavy * bit of available RAM. A RiddleGame needs to be closed as soon as it is not used anymore and should free any resources. * Though users are encouraged to no longer hold onto a RiddleGame object as it is visible.<br> * RiddleGames should be able to fully reconstruct their state to offer an optimal experience, though some abbreviations * might be required as there might by a lot of input data. * Created by daniel on 29.04.15. */ public abstract class RiddleGame { /** * The dimension for a snapshot bitmap for unsolved RiddleGames that get closed. */ protected static final Dimension SNAPSHOT_DIMENSION = new Dimension(32, 32); public static final int BASE_SCORE_MULTIPLIER = 1; //should not change private final Riddle mRiddle; // should be hidden private final RiddleController mRiddleController; protected final Image mImage; // image with hash of mRiddle.mCore.imageHash private SolutionInput mSolutionInput; // input keyboard used, most likely choosing letters by clicking on them protected Bitmap mBitmap; // the correctly scaled bitmap of the image to work with // note: full galaxy s2 display 480x800 pixel, hdpi (scaling of 1.5 by default) protected final RiddleConfig mConfig; private boolean mForbidBonus; private RiddleScore mCalculatedScore; /** * Creates a new RiddleGame, decorating the given riddle, using the given bitmap loaded from the riddle's image. * Will invoke the initBitmap(), initSolutionInput() and initAchievementData() hooks in this order. * * @param riddle The riddle to decorate. * @param image The image associated to the riddle. * @param bitmap The image's bitmap. * @param res A resources object to load assets. * @param config The config to use or describe the riddle. * @param listener The listener to inform about progress (important if loading takes some time). */ protected RiddleGame(Riddle riddle, Image image, Bitmap bitmap, Resources res, RiddleConfig config, PercentProgressListener listener) { if (riddle == null || bitmap == null || res == null || listener == null || config == null || image == null) { throw new IllegalArgumentException("Null argument for InitializedRiddle given."); } mConfig = config; mImage = image; mRiddle = riddle; mRiddleController = makeController(); mBitmap = bitmap; initBitmap(res, listener); initSolutionInput(); initAchievement(); } public void setForbidBonus() { mForbidBonus = true; } /** * Creates the RiddleController using the RiddleConfig's controller factory or a * default controller if none supplied. * * @return A new RiddleController for this RiddleGame and Riddle. */ private RiddleController makeController() { if (mConfig == null || mConfig.mControllerFactory == null) { return RiddleControllerFactory.INSTANCE.makeController(this, mRiddle); } else { return mConfig.mControllerFactory.makeController(this, mRiddle); } } /** * If this game is not yet closed, this is when the loaded bitmap object is still valid. * * @return If the game was not yet closed. */ protected boolean isNotClosed() { return mBitmap != null; } /** * Returns the current state associated with the riddle (probably loaded after restarting the app). * * @return A compacter holding the current state or null if the RiddleGame has no state saved (probably new riddle). */ protected Compacter getCurrentState() { String state = mRiddle.getCurrentState(); if (TextUtils.isEmpty(state)) { return null; } else { return new Compacter(state); } } private String getSolutionData() { return mRiddle.getSolutionData(); } /** * Initializes the solution input. This should only be invoked on unsolved riddles. By default * this creates a new SolutionInput for the type using the solution entered to solve the riddle. * If the riddle is not yet solved this reconstructs the previous SolutionInput. */ private void initSolutionInput() { if (mRiddle.isSolved()) { Solution sol; try { sol = new Solution(new Compacter(getSolutionData())); } catch (CompactedDataCorruptException e) { Log.e("Riddle", "Could not load solution " + e); sol = null; } if (sol != null) { mSolutionInput = SolutionInputManager.getSolutionInput(mRiddle.getType(), sol); } } else { String solutionData = getSolutionData(); if (!TextUtils.isEmpty(solutionData)) { try { mSolutionInput = SolutionInputManager.reconstruct(new Compacter(solutionData)); } catch (CompactedDataCorruptException e) { Log.e("Riddle", "Could not load solution input " + e); mSolutionInput = null; } } if (mSolutionInput == null) { mSolutionInput = SolutionInputManager.getSolutionInput(mRiddle.getType(), mImage.getSolution(Language.getInstance().getTongue())); } } } /** * Returns the image used. * * @return The riddle's image. */ protected Image getImage() { return mImage; } public void addAnimation(@NonNull RiddleAnimation animation) { mRiddleController.addAnimation(animation); } public void addAnimation(@NonNull RiddleAnimation animation, long delay) { mRiddleController.addAnimation(animation, delay); } public final synchronized void close() { int solved = mSolutionInput.estimateSolvedValue(); int score = 0; String currentState = null; String solutionData; if (solved >= Solution.SOLVED_COMPLETELY) { score = getGainedScore(false).getTotalScore(); solutionData = mSolutionInput.getCurrentUserSolution().compact(); } else { currentState = compactCurrentState(); solutionData = mSolutionInput.compact(); } AchievementDataRiddleGame gameData = mRiddle.getType().getAchievementDataGame(); gameData.closeGame(solved); AchievementDataRiddleType typeData = mRiddle.getType().getAchievementData(null); if (typeData != null && solved >= Solution.SOLVED_COMPLETELY) { typeData.putValue(AchievementDataRiddleType.KEY_BEST_SOLVED_TIME, gameData.getValue(AchievementDataRiddleGame.KEY_PLAYED_TIME, Long.MAX_VALUE), -1); } AchievementData achievementDataRaw = mConfig.mAchievementGameData; if (BuildConfig.DEBUG && achievementDataRaw != gameData) { throw new IllegalStateException("Config and type's achievement data not the same!"); } String achievementData = achievementDataRaw == null ? null : achievementDataRaw.compact(); mRiddle.onClose(solved, score, currentState, achievementData, solutionData); ImageUtil.CACHE.makeReusable(mBitmap); mBitmap = null; onClose(); } protected void onClose() { } /** * Makes a new snapshot of this RiddleGame. By default none is made. * * @return null */ protected Bitmap makeSnapshot() { return null; } private void initAchievement() { String achievementData = mRiddle.getAchievementData(); Compacter achievementCmp = TextUtils.isEmpty(achievementData) ? null : new Compacter(achievementData); mRiddle.getType().getAchievementDataGame().loadGame(achievementCmp); Long customValue = Image.ORIGIN_IS_THE_APP.equalsIgnoreCase(mRiddle.getOrigin()) ? 0L : 1L; mRiddle.getType().getAchievementDataGame().putValue(GameAchievement.KEY_DATA_IS_OF_CUSTOM_GAME, customValue, AchievementProperties.UPDATE_POLICY_ALWAYS); AchievementDataRiddleType dataType = mRiddle.getType().getAchievementData(null); if (dataType != null) { dataType.putValue(GameAchievement.KEY_DATA_IS_OF_CUSTOM_GAME, customValue, AchievementProperties.UPDATE_POLICY_ALWAYS); } initAchievementData(); } protected abstract void initAchievementData(); /** * Calculates the gained score. Calculations should be robust and produce the same result if * nothing major happens to the game. * * @return A RiddleScore. */ private synchronized @NonNull RiddleScore calculateGainedScore() { final int scoreMultiplicator = BASE_SCORE_MULTIPLIER; final long timeTaken = mConfig.mAchievementGameData.getCurrentPlayedTime(); int base = mRiddle.getType().getBaseScore(timeTaken); boolean forbidBonus = mForbidBonus; if (isCustom()) { forbidBonus = true; base = mRiddle.isRemade() ? TypesHolder.SCORE_MINIMAL : 0; } RiddleScore.Builder builder = forbidBonus ? new RiddleScore.NoBonusBuilder() : new RiddleScore.Builder(); addBonusReward(builder); return builder .setBase(base) .setMultiplicator(scoreMultiplicator) .build(); } protected void addBonusReward(@NonNull RiddleScore.Rewardable rewardable) { // can be overwritten for specific game } public RiddleScore getGainedScore(boolean forceRefresh) { if (mCalculatedScore == null || forceRefresh) { mCalculatedScore = calculateGainedScore(); } return mCalculatedScore; } protected final boolean isCustom() { return mRiddle.isCustom(); } public abstract void draw(Canvas canvas); public boolean onOrientationEvent(float azimuth, float pitch, float roll) { return false; } public void enableNoOrientationSensorAlternative() { } public boolean requiresPeriodicEvent() { return false; } public void onPeriodicEvent(long updateTime) { } protected abstract void initBitmap(Resources res, PercentProgressListener listener); public abstract boolean onMotionEvent(MotionEvent event); /** * Each riddle should try its best to preserve its state in a compact form that can be restored if * the user reopens the riddle from list of unsolved or simply after closing the app. Restarting * everything will lead to frustration. * * @return Data to restore the exact current state of the riddle. */ protected abstract @NonNull String compactCurrentState(); public void initViews(RiddleView riddleView, SolutionInputView solutionView, SolutionInputListener listener) { riddleView.setController(mRiddleController); solutionView.setSolutionInput(mSolutionInput, listener); } public boolean requiresOrientationSensor() { return false; } public void onGotVisible() { } public ParticleSystem makeParticleSystem(Resources res, int maxParticles, int drawableResId, long timeToLive) { return mRiddleController.makeParticleSystem(res, maxParticles, drawableResId, timeToLive); } public int getRemadeCount() { return mRiddle.getRemadeCount(); } }