/**
* Copyright © 2011,2013 Konstantin Livitski
*
* This file is part of n-Puzzle application. n-Puzzle application is free
* software; you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* n-Puzzle application contains adaptations of artwork covered by the Creative
* Commons Attribution-ShareAlike 3.0 Unported license. Please refer to the
* NOTICE.md file at the root of this distribution or repository for licensing
* terms that apply to that artwork.
*
* n-Puzzle application is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* n-Puzzle application; if not, see the LICENSE/gpl.txt file of this distribution
* or visit <http://www.gnu.org/licenses>.
*/
package name.livitski.games.puzzle.android.model;
import java.io.Serializable;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import name.livitski.games.puzzle.android.ImageSource;
import name.livitski.games.puzzle.android.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
/**
* A container for n-puzzle game data.
*/
public class Game implements MoveListener, TileOnTargetListener
{
/** Returns the difficulty level of this game. */
public Level getDifficulty()
{
return difficulty;
}
/** Returns this game's board. */
public Board getBoard()
{
return board;
}
/** Returns the number of moves made during this game. */
public int getMoveCount()
{
return moveCount;
}
/** Tells whether the puzzle is solved. */
public boolean isSolved()
{
return board.getTileCount() == score;
}
/** Tells whether the puzzle has an image assigned. */
public boolean isImageSelected()
{
return null != selectedImageId;
}
public Serializable getSelectedImageId()
{
return selectedImageId;
}
/**
* Assigns an image to the puzzle. Does not load
* the new image. Call {@link #updateImage()} to load the
* image, update aspect ratio and tiles.
* @param id id of an image resource or name of the cached image file
*/
public void setSelectedImage(Serializable id)
{
selectedImageId = id;
clearImages();
}
public void resetSelectedImage()
{
selectedImageId = null;
clearImages();
}
public float getImageAspectRatio()
{
if (Float.isNaN(imageAspectRatio))
throw new IllegalStateException("No image loaded");
return imageAspectRatio;
}
public void setTileSize(int width, int height)
{
tileWidth = width;
tileHeight = height;
}
public boolean isStarted()
{
return started;
}
public void preview()
{
if (started)
throw new IllegalStateException("The game has been started, cannot show a preview");
board.placeTilesOnTarget();
}
public void start()
{
this.moveCount = 0;
board.placeTilesRandom();
if (!started)
{
this.score = board.getScore();
board.addMoveListener(this);
board.addTileOnTargetListener(this);
}
this.started = true;
}
public void save(Editor settings)
{
settings.putString(DIFFICULTY_SETTING, getDifficulty().toString());
if (!isImageSelected())
settings.remove(IMAGE_ID_SETTING);
else
{
Serializable id = getSelectedImageId();
if (id instanceof Integer)
settings.putInt(IMAGE_ID_SETTING, (Integer)id);
else
settings.putString(IMAGE_ID_SETTING, id.toString());
}
settings.putString(DIFFICULTY_SETTING, getDifficulty().toString());
if (!isStarted())
{
settings.remove(MOVE_COUNT_SETTING);
settings.remove(BOARD_STATE_SETTING);
}
else
{
settings.putInt(MOVE_COUNT_SETTING, getMoveCount());
settings.putString(BOARD_STATE_SETTING, getBoard().getTileLayout());
}
}
public void load(SharedPreferences state)
{
String layout = state.getString(BOARD_STATE_SETTING, null);
if (null != layout)
{
moveCount = state.getInt(MOVE_COUNT_SETTING, 0);
try
{
board.placeTiles(layout);
}
catch (RuntimeException badLayout)
{
Log.w(getClass().getName(), "Error loading game, parsing failed for \""
+ BOARD_STATE_SETTING + '"', badLayout);
board.placeTilesRandom();
}
if (!started)
{
score = board.getScore();
board.addMoveListener(this);
board.addTileOnTargetListener(this);
}
this.started = true;
}
}
public void updateImageSize(Context context)
throws ImageProcessingException
{
if (null == selectedImageId)
throw new IllegalStateException("No image selected");
try
{
// BitmapFactory.Options request = new BitmapFactory.Options();
// request.inJustDecodeBounds = true;
// BitmapFactory.decodeResource(context.getResources(), selectedImageId, request);
// if (0 <= request.outWidth && 0 <= request.outHeight)
Bitmap fullImage = loadFullImage(context);
if (null != fullImage)
imageAspectRatio = // (float)request.outWidth / request.outHeight;
(float)fullImage.getWidth() / fullImage.getHeight();
else
imageAspectRatio = Float.NaN;
}
catch (Resources.NotFoundException e)
{
throw new ImageProcessingException("Resource not found: 0x"
+ Integer.toHexString((Integer)selectedImageId), e);
}
// if (null == fullImage)
if (Float.NaN == imageAspectRatio)
throw new ImageProcessingException("Error loading image "
+ selectedImageId);
}
public void loadImage(final Context context)
throws ImageProcessingException
{
if (null == selectedImageId)
throw new IllegalStateException("No image selected");
if (0 > tileHeight || 0 > tileWidth)
throw new IllegalStateException("Target size is not set");
// load and scale the image
final int boardSize = board.getSize();
try
{
Bitmap fullImage = loadFullImage(context);
if (null == fullImage)
throw new ImageProcessingException("Error loading image "
+ selectedImageId);
scaledImage = Bitmap.createScaledBitmap(
fullImage, tileWidth * boardSize, tileHeight * boardSize, false);
}
catch (Resources.NotFoundException e)
{
throw new ImageProcessingException("Resource not found: 0x"
+ Integer.toHexString((Integer)selectedImageId), e);
}
// slice the image
board.forEachTile(new Board.TileHandler() {
public void processTile(Tile tile)
{
Drawable image;
if (0 == tile.getNumber())
image = new ColorDrawable(context.getResources().getColor(R.color.blank_tile));
else
{
Bitmap slice = Bitmap.createBitmap(
scaledImage,
tileWidth * tile.getTargetColumn(),
tileHeight * tile.getTargetRow(),
tileWidth,
tileHeight
);
image = new BitmapDrawable(context.getResources(), slice);
}
tile.setDrawable(image);
}
});
}
public void tileMoved(Tile from, Tile to)
{
moveCount++;
}
public void tileOnTargetStateChanged(Tile tile, boolean onTarget)
{
score += onTarget ? 1 : -1;
}
/**
* Creates a new game with specified difficulty level.
*/
public Game(Level difficulty)
{
this.difficulty = difficulty;
board = new Board(difficulty.getBoardSize());
}
/**
* Creates a new game with the default difficulty level.
*/
public Game()
{
this(DEFAULT_LEVEL);
}
/**
* Difficulty level of a puzzle game.
*/
public enum Level {
EASY(3), MEDIUM(4), HARD(5);
public int getBoardSize()
{
return boardSize;
}
public static Level forBoardSize(int boardSize)
{
if (3 > boardSize || 5 < boardSize)
throw new IllegalArgumentException("Unsupported board size " + boardSize);
return values()[boardSize - 3];
}
private Level(int boardSize)
{
this.boardSize = boardSize;
}
private int boardSize;
}
public static final Level DEFAULT_LEVEL = Level.MEDIUM;
public static final String IMAGE_ID_SETTING = "image_id";
public static final String DIFFICULTY_SETTING = "difficulty";
protected static final String BOARD_STATE_SETTING = "tiles";
protected static final String MOVE_COUNT_SETTING = "move_count";
private void clearImages()
{
fullImageCache = null;
scaledImage = null;
imageAspectRatio = Float.NaN;
board.forEachTile(TILE_IMAGE_REMOVER);
}
private Bitmap loadFullImage(Context context)
{
Bitmap fullImage = null;
if (null != fullImageCache)
fullImage = fullImageCache.get();
if (null == fullImage)
{
fullImage = selectedImageId instanceof Integer
? BitmapFactory.decodeResource(context.getResources(), (Integer)selectedImageId)
: BitmapFactory.decodeFile(
ImageSource.imageFileForId((String)selectedImageId, context).getAbsolutePath());
fullImageCache = null == fullImage ? null : new SoftReference<Bitmap>(fullImage);
}
return fullImage;
}
private Level difficulty;
private Board board;
private int moveCount, score;
private Serializable selectedImageId;
private boolean started;
private int tileWidth = -1, tileHeight = -1;
private float imageAspectRatio = Float.NaN;
private Bitmap scaledImage;
private Reference<Bitmap> fullImageCache;
private static final Board.TileHandler TILE_IMAGE_REMOVER = new Board.TileHandler() {
public void processTile(Tile tile)
{
tile.setDrawable(null);
}
};
}