/*
* 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.games;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import dan.dit.whatsthat.R;
import dan.dit.whatsthat.achievement.AchievementDataEvent;
import dan.dit.whatsthat.achievement.AchievementProperties;
import dan.dit.whatsthat.image.Image;
import dan.dit.whatsthat.riddle.Riddle;
import dan.dit.whatsthat.riddle.RiddleConfig;
import dan.dit.whatsthat.riddle.achievement.holders.AchievementMemory;
import dan.dit.whatsthat.riddle.control.RiddleGame;
import dan.dit.whatsthat.riddle.control.RiddleScore;
import dan.dit.whatsthat.riddle.types.TypesHolder;
import dan.dit.whatsthat.system.RiddleFragment;
import dan.dit.whatsthat.util.general.BuildException;
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.field.Field2D;
import dan.dit.whatsthat.util.field.FieldElement;
import dan.dit.whatsthat.util.image.BitmapUtil;
import dan.dit.whatsthat.util.image.Dimension;
import dan.dit.whatsthat.util.image.ImageUtil;
/**
* Created by daniel on 01.08.15.
*/
public class RiddleMemory extends RiddleGame {
public static final int DEFAULT_FIELD_X = 8;
public static final int DEFAULT_FIELD_Y = 7; // one dimension must be a multiple of 2!
private static final int TILE_IN_PATH_COLOR = Color.YELLOW;
private static final int MAX_BLACK_FIELDS_FOR_SCORE_BONUS = 0;
private Field2D<MemoryCard> mField;
private Dimension mFieldDimension;
private Paint mCardBorderPaint;
private int mPeakedCards;
private int mFieldX;
private int mFieldY;
private SparseArray<Bitmap> mCoveredCardBitmap;
private Bitmap mFieldBitmap;
private Canvas mFieldCanvas;
private Bitmap mBlackUncoveredCardBitmap;
private List<MemoryCard> mPath; // not saved, so must be only relevant for visual aspects
private List<MemoryCard> mExplicitlySelected; // not saved, so must be only relevant for visual aspects
private int mBlackCardsToDraw;
private Paint mCardContentPaint;
private Paint mCardShapePaint;
private Paint mTileInPathPaint;
public RiddleMemory(Riddle riddle, Image image, Bitmap bitmap, Resources res, RiddleConfig config, PercentProgressListener listener) {
super(riddle, image, bitmap, res, config, listener);
}
@Override
protected void initAchievementData() {
}
@Override
protected void addBonusReward(@NonNull RiddleScore.Rewardable rewardable) {
int blackFields = mConfig.mAchievementGameData != null ? mConfig.mAchievementGameData.getValue(AchievementMemory.KEY_GAME_STATE_BLACK_COUNT, 0L).intValue() : 0;
int bonus = (blackFields <= MAX_BLACK_FIELDS_FOR_SCORE_BONUS ? TypesHolder.SCORE_MEDIUM : 0);
int redFields = mConfig.mAchievementGameData != null ? mConfig.mAchievementGameData
.getValue(AchievementMemory.KEY_GAME_STATE_RED_COUNT, 0L).intValue() : 0;
bonus += redFields <= 0 ? TypesHolder.SCORE_MEDIUM : 0;
rewardable.addBonus(bonus);
}
@Override
public void draw(Canvas canvas) {
canvas.drawBitmap(mFieldBitmap, 0, 0, null);
}
private void drawField() {
mBlackCardsToDraw = 1;
mField.drawField(mFieldCanvas);
}
@Override
public Bitmap makeSnapshot() {
int width = SNAPSHOT_DIMENSION.getWidthForDensity(mConfig.mScreenDensity);
int height = SNAPSHOT_DIMENSION.getHeightForDensity(mConfig.mScreenDensity);
return BitmapUtil.resize(mFieldBitmap, width, height);
}
@Override
protected void initBitmap(Resources res, PercentProgressListener listener) {
mFieldBitmap = Bitmap.createBitmap(mConfig.mWidth, mConfig.mHeight, Bitmap.Config.ARGB_8888);
mFieldCanvas = new Canvas(mFieldBitmap);
mExplicitlySelected = new ArrayList<>(5);
Compacter cmp = getCurrentState();
mFieldX = DEFAULT_FIELD_X;
mFieldY = DEFAULT_FIELD_Y;
if (cmp != null && cmp.getSize() > 1) {
try {
mFieldX = cmp.getInt(0);
mFieldY = cmp.getInt(1);
} catch (CompactedDataCorruptException e) {
cmp = null;
}
}
float fieldWidth = mConfig.mWidth / mFieldX;
float fieldHeight = mConfig.mHeight / mFieldY;
try {
mField = new MemoryBuilder(mFieldX, mFieldY).build(fieldWidth, fieldHeight);
} catch (BuildException be) {
Log.e("Riddle", "Failed building field for RiddleMemory: " + be);
throw new RuntimeException(be);
}
mFieldDimension = new Dimension((int) mField.getFieldWidth(), (int) mField.getFieldHeight());
mCardBorderPaint = new Paint();
mCardBorderPaint.setColor(Color.BLACK);
mCardBorderPaint.setStyle(Paint.Style.STROKE);
mCardContentPaint = new Paint();
mCardShapePaint = new Paint();
mCardShapePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
mTileInPathPaint = new Paint();
mTileInPathPaint.setColor(TILE_IN_PATH_COLOR);
mTileInPathPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
mCoveredCardBitmap = new SparseArray<>(5);
mCoveredCardBitmap.put(MemoryCard.STATE_COVERED_GREEN, ImageUtil.loadBitmap(res, R.drawable.memory_card_covered_green, mFieldDimension.getWidth(), mFieldDimension.getHeight(), true));
mCoveredCardBitmap.put(MemoryCard.STATE_COVERED_YELLOW, ImageUtil.loadBitmap(res, R.drawable.memory_card_covered_yellow, mFieldDimension.getWidth(), mFieldDimension.getHeight(), true));
mCoveredCardBitmap.put(MemoryCard.STATE_COVERED_RED, ImageUtil.loadBitmap(res, R.drawable.memory_card_covered_red, mFieldDimension.getWidth(), mFieldDimension.getHeight(), true));
mCoveredCardBitmap.put(MemoryCard.STATE_COVERED_BLACK, ImageUtil.loadBitmap(res, R.drawable.memory_card_covered_black, mFieldDimension.getWidth(), mFieldDimension.getHeight(), true));
mBlackUncoveredCardBitmap = ImageUtil.loadBitmap(res, R.drawable.memory_card_uncovered_black, mFieldDimension.getWidth(), mFieldDimension.getHeight(), true);
initMemoryImages(res, cmp, listener);
for (MemoryCard card : mField) {
if (!card.isPairUncovered() && card.mUncovered) {
mPeakedCards++;
}
}
drawField();
}
private void initMemoryImages(Resources res, Compacter cmp, PercentProgressListener listener) {
final int requiredImages = mFieldX * mFieldY / 2;
List<Image> memoryImages = new ArrayList<>(requiredImages);
Map<String, Image> allImages = new HashMap<>(RiddleFragment.ALL_IMAGES);
// first use the previously used images when reloading
if (cmp != null && cmp.getSize() > 2) {
String imageKeys = cmp.getData(2);
if (!TextUtils.isEmpty(imageKeys)) {
Compacter keys = new Compacter(imageKeys);
for (int i = 0; i < keys.getSize() && i < requiredImages; i++) {
Image curr = allImages.get(keys.getData(i));
if (curr != null && !curr.equals(mImage)) {
memoryImages.add(curr);
}
}
}
}
listener.onProgressUpdate(25);
// if some or all are missing, fill with new random images
boolean missedImages = false;
if (memoryImages.size() < requiredImages) {
missedImages = true;
List<Image> shuffled = new ArrayList<>(allImages.values());
Collections.shuffle(shuffled);
for (int i = 0; i < shuffled.size() && memoryImages.size() < requiredImages; i++) {
Image curr = shuffled.get(i);
if (curr != null && !curr.equals(mImage) && !memoryImages.contains(curr)) {
memoryImages.add(curr);
}
}
if (memoryImages.size() < requiredImages) {
Log.e("Riddle", "Too little images found to build a memory riddle of " + mFieldX + "x" + mFieldY + " and all images: " + allImages);
throw new RuntimeException("Too little images.");
}
}
listener.onProgressUpdate(40);
// now duplicate images and assign to MemoryCards and restoring order or shuffling newly
List<Image> memoryImagesFinal = new ArrayList<>(memoryImages.size() * 2);
boolean[] uncoveredStates = null;
int[] coverStates = null;
boolean shuffled = false;
if (!missedImages && cmp != null && cmp.getSize() > 5) {
Compacter imagePosition = new Compacter(cmp.getData(3));
Compacter imageDoppelgangerPosition = new Compacter(cmp.getData(4));
Compacter coverStatesRaw = new Compacter(cmp.getData(5));
Image[] images = new Image[requiredImages * 2];
uncoveredStates = new boolean[requiredImages * 2];
shuffled = true;
coverStates = new int[requiredImages * 2];
try {
for (int i = 0; i < coverStates.length; i++) {
if (i < coverStatesRaw.getSize()) {
coverStates[i] = coverStatesRaw.getInt(i);
} else {
coverStates[i] = MemoryCard.STATE_COVERED_GREEN;
}
}
} catch (CompactedDataCorruptException e) {
Log.e("Riddle", "Cover state data corrupt: " + e);
coverStates = null;
}
for (int i = 0; i < requiredImages; i++) {
try {
int pos1 = imagePosition.getInt(i);
int pos2 = imageDoppelgangerPosition.getInt(i);
if (pos1 < 0 && -(pos1+1) < uncoveredStates.length) {
pos1 = -(pos1+1);
uncoveredStates[pos1] = true;
}
if (pos2 < 0 && -(pos2+1) < uncoveredStates.length) {
pos2 = -(pos2+1);
uncoveredStates[pos2] = true;
}
if (pos1 < images.length && pos2 < images.length) {
images[pos1] = memoryImages.get(i);
images[pos2] = images[pos1];
} else {
Log.e("Riddle", "Wrong position for memory image: " + pos1 + " or " + pos2 + " for required images: " + requiredImages);
shuffled = false;
break;
}
} catch (CompactedDataCorruptException cde) {
Log.e("Riddle", "Error extracting positions of memory riddle." + cde);
shuffled = false;
break;
}
}
if (shuffled) {
memoryImagesFinal.addAll(Arrays.asList(images));
}
}
listener.onProgressUpdate(45);
if (!shuffled) {
uncoveredStates = null;
memoryImagesFinal.clear();
memoryImagesFinal.addAll(memoryImages);
memoryImagesFinal.addAll(memoryImages);
Collections.shuffle(memoryImagesFinal);
}
// now init the MemoryCard pairs, restoring cover state and uncovered attribute if available
listener.onProgressUpdate(50);
int bitmapDeltaX = -(mFieldBitmap.getWidth() - mBitmap.getWidth()) / 2;
int bitmapDeltaY = -(mFieldBitmap.getHeight() - mBitmap.getHeight()) / 2;
for (int index = 0; index < requiredImages * 2; index++) {
MemoryCard card = mField.getField(index % mFieldX, index / mFieldX);
Image image = memoryImagesFinal.get(index);
int doppelgangerIndex = memoryImagesFinal.lastIndexOf(image);
if (index == doppelgangerIndex) {
continue;
}
MemoryCard doppelganger = mField.getField(doppelgangerIndex % mFieldX, doppelgangerIndex / mFieldX);
boolean uncovered1 = false, uncovered2 = false;
if (uncoveredStates != null) {
uncovered1 = uncoveredStates[index];
uncovered2 = uncoveredStates[doppelgangerIndex];
}
int coverState1 = MemoryCard.STATE_COVERED_GREEN, coverState2 = MemoryCard.STATE_COVERED_GREEN;
if (coverStates != null) {
coverState1 = coverStates[index];
coverState2 = coverStates[doppelgangerIndex];
}
Rect source = mField.setFieldRect(new Rect(), card);
source.offset(bitmapDeltaX, bitmapDeltaY);
Rect doppelgangerSource = mField.setFieldRect(new Rect(), doppelganger);
doppelgangerSource.offset(bitmapDeltaX, bitmapDeltaY);
card.initPair(res, image, source, doppelgangerSource, doppelganger, uncovered1, uncovered2, coverState1, coverState2);
listener.onProgressUpdate(50 + (int) (index * 50 / ((double) (requiredImages * 2))));
}
}
@Override
public boolean onMotionEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
MemoryCard card = mField.getFieldByCoordinates(event.getX(), event.getY());
if (card != null) {
if (mPeakedCards >= 2) {
coverNotFoundPairs();
}
mExplicitlySelected.add(card);
card.onClick();
drawField();
return true;
}
}
return false;
}
private void coverNotFoundPairs() {
for (MemoryCard allCard : mField) {
allCard.cover();
}
mPath = null;
setGameAchievementValue(AchievementMemory.KEY_GAME_PATH_LENGTH, 0L);
mExplicitlySelected.clear();
mPeakedCards = 0;
}
private void setGameAchievementValue(String key, long value) {
mConfig.mAchievementGameData.putValue(key, value, AchievementProperties.UPDATE_POLICY_ALWAYS);
}
private void incrementGameAchievementValue(String key) {
mConfig.mAchievementGameData.increment(key, 1L, 0L);
}
private void incrementGameAchievementValue(String key, long delta) {
mConfig.mAchievementGameData.increment(key, delta, 0L);
}
@NonNull
@Override
protected String compactCurrentState() {
Compacter cmp = new Compacter();
cmp.appendData(mFieldX);
cmp.appendData(mFieldY);
// memory images
List<String> keySet = new LinkedList<>();
for (MemoryCard card : mField) {
String hash = card.mMemoryImage.getHash();
if (!keySet.contains(hash)) {
keySet.add(hash);
}
}
Compacter keys = new Compacter(keySet.size());
for (String key : keySet) {
keys.appendData(key);
}
// permutation and cover states of images
Compacter cardPositions = new Compacter(keySet.size());
Compacter cardDoppelgangerPositions = new Compacter(keySet.size());
List<Image> imageList = new ArrayList<>(mField.getFieldCount());
for (MemoryCard card : mField) {
imageList.add(card.mMemoryImage);
}
for (MemoryCard card : mField) {
int index = imageList.indexOf(card.mMemoryImage);
int doppelgangerIndex = imageList.lastIndexOf(card.mMemoryImage);
if (index == -1 || doppelgangerIndex == -1) {
continue;
}
imageList.set(index, null);
imageList.set(doppelgangerIndex, null);
cardPositions.appendData(card.mUncovered ? -(index+1): index);// we encode covered state in index, since 0=-0 increase by 1
cardDoppelgangerPositions.appendData(card.mDoppelganger.mUncovered ? -(doppelgangerIndex+1): doppelgangerIndex);
}
cmp.appendData(keys.compact());
cmp.appendData(cardPositions.compact());
cmp.appendData(cardDoppelgangerPositions.compact());
Compacter coverStates = new Compacter(mField.getFieldCount());
for (MemoryCard card : mField) {
coverStates.appendData(card.mCoverState);
}
cmp.appendData(coverStates.compact());
return cmp.compact();
}
private class MemoryCard extends FieldElement {
public static final int STATE_COVERED_GREEN = 0;
public static final int STATE_COVERED_YELLOW = 1;
public static final int STATE_COVERED_RED = 2;
public static final int STATE_COVERED_BLACK = 3;
private boolean mUncovered;
private MemoryCard mDoppelganger;
private Rect mBitmapSource;
private Image mMemoryImage;
private Bitmap mMemoryBitmap;
private int mCoverState;
@Override
public boolean isBlocked() {
return mUncovered;
}
public void initPair(Resources res, Image memoryImage, Rect bitmapSource, Rect doppelgangerbitmapSource, MemoryCard doppelganger, boolean uncovered, boolean doppelgangerUncovered, int coverState, int doppelgangerCoverState) {
mDoppelganger = doppelganger;
mDoppelganger.mDoppelganger = this;
mMemoryImage = memoryImage;
mDoppelganger.mMemoryImage = memoryImage;
mMemoryBitmap = mMemoryImage.loadBitmap(res, mFieldDimension, true);
mDoppelganger.mMemoryBitmap = mMemoryBitmap;
mBitmapSource = bitmapSource;
mDoppelganger.mBitmapSource = doppelgangerbitmapSource;
mUncovered = uncovered;
mDoppelganger.mUncovered = doppelgangerUncovered;
mCoverState = coverState;
mDoppelganger.mCoverState = doppelgangerCoverState;
}
public boolean isPairUncovered() {
return mUncovered && mDoppelganger.mUncovered;
}
@Override
public void draw(Canvas canvas, Rect fieldRect) {
boolean isInPath = mPath != null && mPath.contains(this);
int oldColor;
Bitmap cardCover = mCoveredCardBitmap.get(mCoverState);
cardCover = cardCover == null ? mCoveredCardBitmap.get(STATE_COVERED_GREEN) : cardCover;
if (isPairUncovered()) {
canvas.drawBitmap(mBitmap, mBitmapSource, fieldRect, null);
canvas.drawBitmap(cardCover, fieldRect.left, fieldRect.top, mCardShapePaint);
} else if (mUncovered) {
if (mCoverState == STATE_COVERED_BLACK && mBlackCardsToDraw <= 0) {
canvas.drawBitmap(mBlackUncoveredCardBitmap, fieldRect.left, fieldRect.top, null);
} else {
if (mCoverState == STATE_COVERED_BLACK) {
mBlackCardsToDraw--;
}
if (mMemoryBitmap != null) {
canvas.drawBitmap(mMemoryBitmap, fieldRect.left, fieldRect.top, null);
canvas.drawBitmap(cardCover, fieldRect.left, fieldRect.top, mCardShapePaint);
} else {
// emergency case in case bitmap loading failed since we can't fetch a new one easily
oldColor = mCardBorderPaint.getColor();
mCardContentPaint.setColor(mMemoryImage.getAverageARGB());
canvas.drawRect(fieldRect, mCardContentPaint);
canvas.drawBitmap(cardCover, fieldRect.left, fieldRect.top, mCardShapePaint);
mCardContentPaint.setColor(oldColor);
}
}
} else {
canvas.drawBitmap(cardCover, fieldRect.left, fieldRect.top, null);
}
if (isInPath && !mExplicitlySelected.contains(this)) {
canvas.drawRect(fieldRect, mTileInPathPaint);
}
/*
// draw border on top
oldColor = mCardBorderPaint.getColor();
if (isInPath) {
mCardBorderPaint.setColor(Color.YELLOW);
}
fieldRect.set(fieldRect.left-1, fieldRect.top-1, fieldRect.right+1, fieldRect.bottom+1);
canvas.drawRect(fieldRect, mCardBorderPaint);
fieldRect.set(fieldRect.left+1, fieldRect.top+1, fieldRect.right-1, fieldRect.bottom-1);
mCardBorderPaint.setColor(oldColor);*/
}
public boolean uncover() {
if (!mUncovered) {
mUncovered = true;
mPeakedCards++;
if (isPairUncovered()) {
mPeakedCards -= 2;
}
return true;
}
return false;
}
public void onClick() {
mConfig.mAchievementGameData.enableSilentChanges(AchievementDataEvent.EVENT_TYPE_DATA_UPDATE);
boolean uncovered = uncover();
if (uncovered && isPairUncovered()) {
mPath = mField.findPath(this, mDoppelganger, FieldElement.DIRECT_AND_DIAGONAL_NEIGHBORS);
setGameAchievementValue(AchievementMemory.KEY_GAME_PATH_LENGTH, mPath == null ? 0L : mPath.size());
if (mPath != null) {
int uncoveredPairsCount = 0;
for (MemoryCard card : mPath) {
if (card.mCoverState == STATE_COVERED_GREEN) {
if (card.uncover()) {
card.mCoverState--;
}
if (card.isPairUncovered() && card != this && card != mDoppelganger) {
uncoveredPairsCount++;
}
} else {
card.mCoverState--;
}
}
if (uncoveredPairsCount > 0) {
incrementGameAchievementValue(AchievementMemory.KEY_GAME_UNCOVERED_PAIRS_BY_PATH_COUNT, uncoveredPairsCount);
}
}
}
if (uncovered) {
incrementGameAchievementValue(AchievementMemory.KEY_GAME_CARD_UNCOVERED_BY_CLICK_COUNT);
}
int redCount = 0;
int yellowCount = 0;
int greenCount = 0;
int blackCount = 0;
int uncoveredPairCount = 0;
int uncoveredGreenPairCount = 0;
for (MemoryCard card : mField) {
switch (card.mCoverState) {
case STATE_COVERED_GREEN:
greenCount++;
break;
case STATE_COVERED_YELLOW:
yellowCount++;
break;
case STATE_COVERED_RED:
redCount++;
break;
case STATE_COVERED_BLACK:
blackCount++;
break;
}
if (card.isPairUncovered()) {
uncoveredPairCount++;
if (card.mCoverState == STATE_COVERED_GREEN && card.mDoppelganger.mCoverState == STATE_COVERED_GREEN) {
uncoveredGreenPairCount++;
}
}
}
uncoveredPairCount /= 2;
setGameAchievementValue(AchievementMemory.KEY_GAME_STATE_GREEN_COUNT, greenCount);
setGameAchievementValue(AchievementMemory.KEY_GAME_STATE_YELLOW_COUNT, yellowCount);
setGameAchievementValue(AchievementMemory.KEY_GAME_STATE_RED_COUNT, redCount);
setGameAchievementValue(AchievementMemory.KEY_GAME_STATE_BLACK_COUNT, blackCount);
setGameAchievementValue(AchievementMemory.KEY_GAME_UNCOVERED_PAIRS_COUNT, uncoveredPairCount);
setGameAchievementValue(AchievementMemory.KEY_GAME_UNCOVERED_PAIRS_IN_GREEN_STATE_COUNT, uncoveredGreenPairCount);
mConfig.mAchievementGameData.disableSilentChanges();
}
public void cover() {
if (!isPairUncovered() && mUncovered) {
mPeakedCards--;
mUncovered = false;
if (mCoverState < MemoryCard.STATE_COVERED_BLACK) {
mCoverState++;
}
mCoverState = Math.max(mCoverState, STATE_COVERED_GREEN); //just to make sure we are never in an illegal state, works without though
}
}
@Override
public String toString() {
return "Card at " + mX + "/" + mY + " name: " + mMemoryImage.getName();
}
}
private class MemoryBuilder extends Field2D.Builder<MemoryCard> {
public MemoryBuilder(int xCount, int yCount) throws BuildException {
for (int y = 0; y < yCount; y++) {
nextRow();
for (int x = 0; x < xCount; x++) {
nextElement(new MemoryCard());
}
}
}
@Override
protected MemoryCard[][] makeArray(int rows, int columns) {
return new MemoryCard[rows][columns];
}
}
}