/**
* 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;
import java.io.Serializable;
import java.util.Map;
import name.livitski.games.puzzle.android.model.Board;
import name.livitski.games.puzzle.android.model.Game;
import name.livitski.games.puzzle.android.model.Game.Level;
import name.livitski.games.puzzle.android.model.ImageProcessingException;
import name.livitski.games.puzzle.android.model.Move;
import name.livitski.games.puzzle.android.model.MoveListener;
import name.livitski.games.puzzle.android.model.Tile;
import android.app.Dialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
/**
* User's interface to the puzzle.
*/
public class GamePlay extends Activity implements OnClickListener, OnLongClickListener, MoveListener
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
initLayout();
initGame();
initBoard();
}
/**
* Handles user's clicks and taps on the board.
*/
public void onClick(View v)
{
final Object tag = v.getTag();
if (null == previewTimer && tag instanceof Tile)
{
final Board board = getBoardModel();
final Move move = board.permittedMoveFor((Tile) tag);
if (null != move)
{
board.move(move);
if (game.isSolved())
congratulate();
}
else
{
wrongClick();
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.game_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case R.id.item_reshuffle:
restart(false);
break;
case R.id.item_change_picture:
restart(true);
break;
case R.id.item_difficulty_hard:
if (game.getDifficulty() != Game.Level.HARD)
restart(Game.Level.HARD);
break;
case R.id.item_difficulty_medium:
if (game.getDifficulty() != Game.Level.MEDIUM)
restart(Game.Level.MEDIUM);
break;
case R.id.item_difficulty_easy:
if (game.getDifficulty() != Game.Level.EASY)
restart(Game.Level.EASY);
break;
default:
return super.onOptionsItemSelected(item);
}
resumeImpl();
return true;
}
/**
* Handles a long click on the blank tile.
*/
public boolean onLongClick(View v)
{
final Object tag = v.getTag();
if (null == previewTimer && tag instanceof Tile && 0 == ((Tile)tag).getNumber())
{
toggleBoardType();
return true;
}
else
return false;
}
/**
* Updates the view when the model posts a tile move notification.
*/
public void tileMoved(final Tile from, final Tile to)
{
// repeat for both tiles
for (Tile tile : new Tile[] { from, to })
{
TextView numericCell = numericCells[tile.getRow()][tile.getColumn()];
assignTile(numericCell, tile);
ImageView imageCell = imageCells[tile.getRow()][tile.getColumn()];
assignTile(imageCell, tile);
}
}
@Override
protected void onResume()
{
super.onResume();
resumeImpl();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (requestCode)
{
case IMAGE_SELECTION_REQUEST_CODE:
if (RESULT_CANCELED == resultCode)
finish();
else
{
Serializable id = data.getSerializableExtra(ImageSelection.EXTRA_SELECTED_IMAGE_ID_KEY);
if (null == id)
throw new IllegalStateException(
"Image selection page did not return an image");
Level requestedLevel =
(Level)data.getSerializableExtra(ImageSelection.EXTRA_SELECTED_IMAGE_INITIAL_LEVEL);
if (null != requestedLevel && requestedLevel != game.getDifficulty())
restart(requestedLevel);
changeBoardImage(id);
}
break;
case CONGRATULATIONS_REQUEST_CODE:
if (RESULT_OK != resultCode)
finish();
break;
default:
throw new IllegalStateException(
"Received a result from unknown activity, code = " + requestCode);
}
}
@Override
protected void onPause()
{
super.onPause();
saveSettings();
}
/** Returns the current board size. */
public int getBoardSize()
{
return getBoardModel().getSize();
}
/**
* Discards the current board state and starts a new game.
* Unless you are switching activities, you have to follow up
* this method with a call to {@link #resumeImpl()} to show
* the new board.
* @param selectNewImage tells the puzzle whether the user
* would like to select a new image
*/
protected void restart(boolean selectNewImage)
{
cancelPreviewTimer();
hideBoard();
if (selectNewImage)
newGame(null);
else
game.start();
initBoard();
}
/**
* Starts a new game with a specific difficulty level. If there
* is an image selected for the current game, uses the same
* image for the new game.
* Unless you are switching activities, you have to follow up
* this method with a call to {@link #resumeImpl()} to show
* the new board.
* @param difficulty difficulty level for the new game
*/
protected void restart(Game.Level difficulty)
{
cancelPreviewTimer();
hideBoard();
Serializable imageId = null;
if (null != game && game.isImageSelected())
imageId = game.getSelectedImageId();
newGame(difficulty);
if (null != imageId)
changeBoardImage(imageId);
initBoard();
}
protected void cancelPreviewTimer()
{
if (null != previewTimer)
{
previewTimer.cancel();
previewTimer = null;
}
}
protected void hideBoard()
{
windowLayout.removeAllViews();
windowLayout.invalidate();
}
protected void showNumericBoard()
{
// force resizeContent() if the board was hidden since onMeasure() events might have been missed
if (0 == windowLayout.getChildCount())
lastWidth = lastHeight = -1;
windowLayout.removeAllViews();
windowLayout.addView(numericBoardView);
windowLayout.invalidate();
}
protected void showImageBoard()
{
// force resizeContent() if the board was hidden since onMeasure() events might have been missed
if (0 == windowLayout.getChildCount())
lastWidth = lastHeight = -1;
windowLayout.removeAllViews();
windowLayout.addView(imageBoardView);
windowLayout.invalidate();
}
/**
* Toggles the "cheat mode" that shows the tiles' numbers.
*/
protected void toggleBoardType()
{
if (0 == windowLayout.getChildCount())
return;
else if (windowLayout.getChildAt(0) == numericBoardView)
showImageBoard();
else
showNumericBoard();
}
protected Board getBoardModel()
{
return game.getBoard();
}
protected void initLayout()
{
windowLayout = new RelativeLayout(this) {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (width != lastWidth || height != lastHeight)
{
// Log.d(
// getClass().getName(),
// "resizing to width = " + Integer.toHexString(widthMeasureSpec) + " ("
// + width + ") " + ", height = "
// + Integer.toHexString(heightMeasureSpec) + " ("
// + height + ")");
if (0 < getChildCount())
resizeContent(width, height);
lastWidth = width;
lastHeight = height;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
};
windowLayout.setBackgroundColor(getResources().getColor(R.color.background));
setContentView(windowLayout, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
protected void initGame()
{
SharedPreferences preferences = getPreferences(MODE_PRIVATE);
Map<String, ?> settings = preferences.getAll();
try
{
if (settings.containsKey(Game.DIFFICULTY_SETTING))
newGame(Game.Level.valueOf((String)settings.get(Game.DIFFICULTY_SETTING)));
else
newGame(null);
if (settings.containsKey(Game.IMAGE_ID_SETTING))
changeBoardImage((Serializable)settings.get(Game.IMAGE_ID_SETTING));
}
catch (RuntimeException invalid)
{
Log.w(getClass().getName(), "Error loading settings", invalid);
newGame(null);
}
game.load(preferences);
}
protected void newGame(Level difficulty)
{
if (null != difficulty)
game = new Game(difficulty);
else if (null == game)
game = new Game();
else
game = new Game(game.getDifficulty());
}
protected void initBoard()
{
numericBoardView = new TableLayout(this);
imageBoardView = new TableLayout(this);
final Board board = getBoardModel();
board.addMoveListener(this);
final int boardSize = getBoardSize();
numericCells = new TextView[boardSize][boardSize];
imageCells = new ImageView[boardSize][boardSize];
for (int i = 0; i < boardSize; i++)
{
TableRow numericRow = new TableRow(this);
TableRow imageRow = new TableRow(this);
for (int j = 0; j < boardSize; j++)
{
TextView numericCell = new TextView(this);
ImageView imageCell = new ImageView(this);
numericCells[i][j] = numericCell;
imageCells[i][j] = imageCell;
initNumericCell(numericCell);
initImageCell(imageCell);
numericRow.addView(numericCell);
imageRow.addView(imageCell);
}
numericBoardView.addView(numericRow);
imageBoardView.addView(imageRow);
}
countDownCell = new TextView(this);
initNumericCell(countDownCell);
}
protected void initImage()
{
if (!game.isImageSelected())
{
Intent imageRequest = new Intent(this, ImageSelection.class);
startActivityForResult(imageRequest, IMAGE_SELECTION_REQUEST_CODE);
}
}
protected void resumeImpl()
{
if (dialogActive)
hideBoard();
else
{
initImage();
if (game.isImageSelected())
{
showImageBoard();
if (!game.isStarted())
showPreview();
}
}
}
protected void saveSettings()
{
Editor settings = getPreferences(MODE_PRIVATE).edit();
game.save(settings);
settings.commit();
}
protected void congratulate()
{
Intent intent = new Intent(this, YouWin.class);
intent.putExtra(YouWin.EXTRA_MOVE_COUNT, game.getMoveCount());
if (game.isImageSelected())
intent.putExtra(ImageSelection.EXTRA_SELECTED_IMAGE_ID_KEY, game.getSelectedImageId());
startActivityForResult(intent, CONGRATULATIONS_REQUEST_CODE);
restart(true);
}
protected void showPreview()
{
if (null != previewTimer)
return;
game.preview();
int lastIndex = getBoardSize() - 1;
final View blankTileCell = imageCells[lastIndex][lastIndex];
final ViewGroup parent = (ViewGroup)blankTileCell.getParent();
parent.removeView(blankTileCell);
countDownCell.setTextColor(getResources().getColor(R.color.countdown_tile_text));
parent.addView(countDownCell);
previewTimer = new CountDownTimer(3000, 500) {
@Override
public void onTick(long millisUntilFinished)
{
int secondsRemaining = Math.round((float)millisUntilFinished / 1000);
countDownCell.setText(Integer.toString(secondsRemaining));
}
@Override
public void onFinish()
{
hideBoard();
parent.removeView(countDownCell);
parent.addView(blankTileCell);
game.start();
showImageBoard();
previewTimer = null;
}
};
previewTimer.start();
}
protected void wrongClick()
{
if (null != dimTimer)
return;
dimTimer = new CountDownTimer(500L, 500L)
{
@Override
public void onTick(long millisUntilFinished) {}
@Override
public void onFinish()
{
dimImmovableTiles(false);
dimTimer = null;
}
};
dimImmovableTiles(true);
dimTimer.start();
}
protected void dimImmovableTiles(boolean on)
{
final Board board = getBoardModel();
final int boardSize = board.getSize();
final int color = getResources().getColor(R.color.tile_dimmer);
for (int r = 0; r < boardSize; r++)
for (int c = 0; c < boardSize; c++)
{
final ImageView cell = imageCells[r][c];
final Object tag = cell.getTag();
if (tag instanceof Tile
&& null == board.permittedMoveFor((Tile)tag)
&& 0 != ((Tile)tag).getNumber())
{
if (on)
cell.setColorFilter(color);
else
cell.clearColorFilter();
}
}
}
protected void resizeContent(final int screenWidth, final int screenHeight)
{
if (null == game)
throw new IllegalStateException(this + " must be initialized with onCreate()");
final float imageRatio = game.isImageSelected() ? game.getImageAspectRatio() : 1f;
final int boardSize = getBoardSize();
final DisplayMetrics metrics = getResources().getDisplayMetrics();
// border width 1 dp rounded up to nearest whole pixels
final int borderWidth = (int) Math.ceil(metrics.density);
// calculate spacing allotment
final int spacing = borderWidth * 2 * boardSize;
if (screenWidth < spacing + boardSize || screenHeight < spacing + boardSize)
throw new UnsupportedOperationException("Screen size (" + screenWidth
+ " x " + screenHeight + ") too small for a board of " + boardSize
+ " rows");
final float adjustedScreenRatio = (float) (screenWidth - spacing)
/ (screenHeight - spacing);
int width, height;
if (adjustedScreenRatio > imageRatio)
{
// scale to screen height
height = screenHeight;
// fix width = imageRatio * height
width = (int) (imageRatio * height);
if (width < spacing + boardSize)
throw new UnsupportedOperationException(
"Need a wider image to make a board: scaled to " + width
+ " pixels, need " + (spacing + boardSize));
}
else
{
// scale to screen width
width = screenWidth;
// fix height = width / imageRatio
height = (int) (width / imageRatio);
if (height < spacing + boardSize)
throw new UnsupportedOperationException(
"Need a taller image to make a board: scaled to " + height
+ " pixels, need " + (spacing + boardSize));
}
// make the dimensions divisible by row/column count
height -= height % boardSize;
width -= width % boardSize;
RelativeLayout.LayoutParams boardLayoutParams = new RelativeLayout.LayoutParams(
width, height);
boardLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
numericBoardView.setLayoutParams(boardLayoutParams);
imageBoardView.setLayoutParams(boardLayoutParams);
// load and resize the image
width = width / boardSize - 2 * borderWidth;
height = height / boardSize - 2 * borderWidth;
game.setTileSize(width, height);
if (game.isImageSelected())
try
{
game.loadImage(this);
}
catch (ImageProcessingException failure)
{
game.resetSelectedImage();
Log.e(getClass().getName(), failure.getMessage(), failure);
hideBoard();
alert(R.string.image_load_error);
return;
}
// size and fill cell views
TableRow.LayoutParams cellParams = new TableRow.LayoutParams(width, height);
cellParams.setMargins(borderWidth, borderWidth, borderWidth, borderWidth);
float fontSize = width * 4f / 3;
if (fontSize > height)
fontSize = height;
fontSize *= .5f;
final Board board = getBoardModel();
for (int i = 0; i < boardSize; i++)
{
for (int j = 0; j < boardSize; j++)
{
TextView numericCell = numericCells[i][j];
numericCell.setLayoutParams(cellParams);
numericCell.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
ImageView imageCell = imageCells[i][j];
imageCell.setLayoutParams(cellParams);
Tile tile = board.getTileAt(i, j);
assignTile(numericCell, tile);
assignTile(imageCell, tile);
}
}
countDownCell.setLayoutParams(cellParams);
countDownCell.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
}
@Override
protected void onPrepareDialog(int id, Dialog dialog)
{
super.onPrepareDialog(id, dialog);
dialogActive = true;
}
@Override
protected void onDialogResponse(int dialogId, int response)
{
dialogActive = false;
resumeImpl();
}
@Override
protected void onDialogCancel(int dialogId)
{
if (dialogActive)
{
dialogActive = false;
resumeImpl();
}
}
protected void alert(int messageId)
{
String msg = getResources().getString(messageId);
alert(msg);
}
private void assignTile(final TextView cell, final Tile tile)
{
final int number = tile.getNumber();
cell.setText(0 == number ? " " : Integer.toString(number));
cell.setTag(tile);
cell.setBackgroundColor(getResources().getColor(
0 == number ? R.color.blank_tile : R.color.numeric_tile_background));
}
private void assignTile(final ImageView cell, final Tile tile)
{
cell.setImageDrawable(tile.getDrawable());
cell.setTag(tile);
}
private void initImageCell(ImageView imageCell)
{
imageCell.setOnClickListener(this);
imageCell.setOnLongClickListener(this);
}
private void initNumericCell(TextView numericCell)
{
numericCell.setGravity(Gravity.CENTER);
numericCell.setTextColor(getResources().getColor(R.color.numeric_tile_text));
numericCell.setOnClickListener(this);
numericCell.setOnLongClickListener(this);
}
private void changeBoardImage(Serializable id)
{
game.setSelectedImage(id);
try
{
game.updateImageSize(this);
}
catch (ImageProcessingException failure)
{
game.resetSelectedImage();
Log.e(getClass().getName(), failure.getMessage(), failure);
alert(R.string.image_load_error);
}
}
private Game game;
private RelativeLayout windowLayout;
private TableLayout numericBoardView, imageBoardView;
private TextView[][] numericCells;
private ImageView[][] imageCells;
private boolean dialogActive;
private int lastWidth = -1, lastHeight = -1;
private CountDownTimer previewTimer, dimTimer;
private TextView countDownCell;
private static final int IMAGE_SELECTION_REQUEST_CODE = 1;
private static final int CONGRATULATIONS_REQUEST_CODE = 2;
}