Java tutorial
/* * Copyright (C) 2015 Jeremy Brown. Released under the Non-Profit Open Software License version 3.0 (NPOSL-3.0) */ package com.mischivous.wormysharpyloggy.wsl; import android.annotation.TargetApi; import android.app.Activity; import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.res.ResourcesCompat; import android.util.Log; import android.view.Gravity; import android.view.View; import android.widget.*; import com.mischivous.wormysharpyloggy.wsl.data.OptionsHelper; import com.mischivous.wormysharpyloggy.wsl.data.PlayerDataDbHelper; import com.mischivous.wormysharpyloggy.wsl.model.Game; import com.mischivous.wormysharpyloggy.wsl.model.GameOverEvent; import com.mischivous.wormysharpyloggy.wsl.model.GameType; import com.mischivous.wormysharpyloggy.wsl.model.Tile; import com.mischivous.wormysharpyloggy.wsl.util.GameOverListener; import com.mischivous.wormysharpyloggy.wsl.util.SetHelper; import com.mischivous.wormysharpyloggy.wsl.view.ShadedImageView; import java.util.Timer; import java.util.TimerTask; /** * The Activity that displays the main game screen during play * and handles game logic. * * @author Jeremy Brown * @version 1.0 * @since June 30, 2015 */ public class GameScreen extends Activity implements View.OnClickListener, GameOverListener { // Variables used by all game types. private static final String TAG = "GameScreen"; private static final int boardSize = 9; private static final Handler handler = new Handler(); private final ShadedImageView[] tiles = new ShadedImageView[boardSize]; private TextView timeView; private Tile[] selected; private Game game; private final Runnable updateClock = new Runnable() { @Override public void run() { // Setup the time display for normal/Powerset game types if (game.GetGameType() == GameType.Normal || game.GetGameType() == GameType.PowerSet) { long elapsedSeconds = game.GetElapsedTime() / 1000; timeView.setText(String.format("%d:%02d", elapsedSeconds / 60, elapsedSeconds % 60)); // Setup the time display for time attack game type } else { long timeRemaining = game.GetTimeRemaining(getApplicationContext()) / 1000; timeView.setText(String.format("%d:%02d", timeRemaining / 60, timeRemaining % 60)); } } }; // Variables used by normal/time attack types private ImageView[][] found; /** * {@inheritDoc} */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Choose the appropriate layout for the game type. GameType type = (GameType) getIntent().getExtras().get("type"); if (type == GameType.PowerSet) { setContentView(R.layout.game_power_layout); } else { setContentView(R.layout.game_standard_layout); } // Choose the correct number of tiles possible for selection // at one time. if (type == GameType.PowerSet) { selected = new Tile[4]; } else { selected = new Tile[3]; } // Initialize the ImageView for each Tile on the Board for (int i = 0; i < boardSize; i++) { int id = getResources().getIdentifier(String.format("Tile%d", i + 1), "id", getPackageName()); tiles[i] = (ShadedImageView) findViewById(id); if (tiles[i] == null) { Log.e(TAG, String.format("Failed to set tiles[%d]. Value is null.", i)); } else { tiles[i].setTag(R.id.TILE_INDEX, i); tiles[i].setOnClickListener(this); } } // Set up the area to store found Sets in normal/time attack // modes and initialize their ImageViews. if (type == GameType.Normal || type == GameType.TimeAttack) { found = new ImageView[OptionsHelper.GetSetCount(this)][3]; for (int set = 0; set < found.length; set++) { for (int tile = 0; tile < found[set].length; tile++) { int id = getResources().getIdentifier(String.format("FoundSet%d_%d", set + 1, tile + 1), "id", getPackageName()); found[set][tile] = (ImageView) findViewById(id); if (found[set][tile] == null) { Log.e(TAG, String.format("Failed to set found[%d][%d]. Value is null.", set, tile)); } } } } StartGame(type); // Initialize the timer timeView = (TextView) findViewById(R.id.TimeView); Timer clockUpdateTimer = new Timer(); clockUpdateTimer.schedule(new TimerTask() { @Override public void run() { handler.post(updateClock); } }, 0, 200); } /** * Initialize a new Game instance of the selected game type. * * @param type The type of Game to initialize */ @TargetApi(17) private void StartGame(@NonNull GameType type) { if (type == null) { throw new IllegalArgumentException("Game type cannot be null."); } game = new Game(type, OptionsHelper.GetSetCount(this), OptionsHelper.GetMinDiff(this), OptionsHelper.GetMaxDiff(this)); game.AddGameOverListener(this); ClearTilesSelected(); for (int i = 0; i < tiles.length; i++) { Drawable d = game.GetTileAt(i).GetDrawable(this); if (d == null) { Log.e(TAG, String.format("Failed to get drawable for tile %d. Value is null.", i)); } else { tiles[i].setImageDrawable(d); Log.d(TAG, String.format("Set tile image for tiles[%d]", i)); } } // Initialize the blanks at the top for normal/time attack modes if (type == GameType.Normal || type == GameType.TimeAttack) { if (found.length > 3) { for (int set = 3; set < found.length; set++) for (int tile = 0; tile < found[set].length; tile++) found[set][tile].setImageDrawable( ResourcesCompat.getDrawable(getResources(), R.mipmap.tile_small_blank, null)); } // Center the Found Sets string RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams( findViewById(R.id.foundSetsTitle).getLayoutParams()); int id = getResources().getIdentifier(String.format("FoundSet%d_1", found.length), "id", getPackageName()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { lp.addRule(RelativeLayout.ALIGN_END, id); } else { lp.addRule(RelativeLayout.ALIGN_RIGHT, id); } findViewById(R.id.foundSetsTitle).setLayoutParams(lp); } else { // Place the first join in the correct location int id = getResources().getIdentifier("joinImage", "id", getPackageName()); ImageView iv = (ImageView) findViewById(id); if (iv == null) { Log.e(TAG, "Failed to set joinImage. Value is null."); } else { iv.setImageDrawable(game.GetNextJoin().GetDrawable(this)); } } // Change elapsed time string to time remaining string for Time Attack if (type == GameType.TimeAttack) { ((TextView) findViewById(R.id.timeTitle)).setText(getString(R.string.remainingString)); } } /** * Gets the selected state of a Tile, by its index. * @param tileIndex The index of the Tile to check for selection * @return Whether or not the Tile is shaded */ private boolean IsTileSelected(@IntRange(from = 0, to = 8) int tileIndex) { if (tileIndex < 0 || tileIndex >= boardSize) { throw new IllegalArgumentException("tileIndex out of bounds."); } else { return tiles[tileIndex].getShaded(); } } /** * Selects a Tile. If the correct number of Tiles have been selected, * check to see if the player has found either a set or a PowerSet, * depending on game mode. * * @param tileIndex The index of the Tile to select or deselect * @param selected The state to set the Tile in */ private void SetTileSelected(@IntRange(from = 0, to = 8) int tileIndex, boolean selected) { if (tileIndex < 0 || tileIndex > boardSize - 1) { return; } else if (IsTileSelected(tileIndex) == selected) { return; } if (selected) { int selectedCount = 1; Tile t = game.GetTileAt(tileIndex); for (int i = 0; i < this.selected.length; i++) { if (this.selected[i] == null) { this.selected[i] = t; break; } else { selectedCount += 1; } } // If we've selected the correct number of Tiles to check // for Sets, check with Game class to see if the given // Set is good. if (game.GetGameType() == GameType.Normal || game.GetGameType() == GameType.TimeAttack) { tiles[tileIndex].setShaded(selected); Log.d(TAG, String.format("Setting tile %d shading = %s", tileIndex, Boolean.valueOf(selected).toString())); if (selectedCount == 3) { // Check if the selected set was found already if (game.WasFoundAlready(this.selected)) { messageUser(getString(R.string.foundSetAlready)); } // If it wasn't, make sure it's a valid set else if (game.IsValidSet(this.selected)) { messageUser(getString(R.string.foundSet)); // Place the found set up top int set = game.GetFoundSetCount() - 1; for (int i = 0; i < 3; i++) { found[set][i].setImageDrawable(this.selected[i].GetSmallDrawable(this)); } // If it wasn't a valid set, inform the player } else { messageUser(getString(R.string.badSet)); } ClearTilesSelected(); } // If we've selected the correct number of Tiles to check // for Powersets, check with Game class to see if the given // Sets are good. } else { if (selectedCount <= 2) { tiles[tileIndex].setShaded(2); } else { tiles[tileIndex].setShaded(3); } Log.d(TAG, String.format("Setting tile %d shading = %s", tileIndex, selectedCount <= 2 ? "Red" : "Blue")); if (selectedCount == 4) { // The Game class queues up the next join upon // finding a valid Powerset, so just set it and go if (game.IsValidSet(this.selected)) { messageUser(getString(R.string.foundSet)); int id = getResources().getIdentifier("joinImage", "id", getPackageName()); if (!game.IsGameOver()) { ImageView iv = (ImageView) findViewById(id); if (iv == null) { Log.e(TAG, "Failed to set joinImage. Value is null."); } else { iv.setImageDrawable(game.GetNextJoin().GetDrawable(this)); } } // If it wasn't a valid set, inform the player } else { messageUser(getString(R.string.badSet)); } ClearTilesSelected(); } } // Deselect the tile and remove it from the selected array } else { tiles[tileIndex].setShaded(selected); Log.d(TAG, String.format("Setting tile %d shading = %s", tileIndex, Boolean.valueOf(selected).toString())); Tile t = game.GetTileAt(tileIndex); for (int i = 0; i < this.selected.length; i++) { if (this.selected[i] == t) { this.selected[i] = null; break; } } } } /** * Deselects all Tiles. */ private void ClearTilesSelected() { for (int i = 0; i < selected.length; i++) { selected[i] = null; } for (int i = 0; i < boardSize; i++) { SetTileSelected(i, false); } } /** * Displays a message to the user in a Toast. * * @param msg The message to display */ private void messageUser(@NonNull String msg) { if (msg == null) { throw new NullPointerException("Message to user cannot be null."); } Toast toast = Toast.makeText(this, msg, (msg.length() > 24 ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT)); toast.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 0); toast.show(); } /** * Event handler for the player selects a Tile. * * @param view The View representing the selected tile */ @Override public void onClick(@NonNull View view) { if (view == null) { throw new NullPointerException("Button clicked cannot be null."); } Object o = view.getTag(R.id.TILE_INDEX); if (view instanceof ImageView && o instanceof Integer) { if (game.IsGameOver()) return; int index = (Integer) o; Log.d(TAG, String.format("Event: onClick for Tile %d", index)); SetTileSelected(index, !IsTileSelected(index)); } } /** * {@inheritDoc} */ @Override public void GameOver(@Nullable GameOverEvent e) { long lastGame = PlayerDataDbHelper.saveOutcome(this, game); Intent i = new Intent(this, SummaryScreen.class); i.putExtra("lastGame", lastGame); startActivity(i); finish(); } /** * Handles pause and unpause game logic, largely blanking the screen. * @param v The pause button, ignorable */ public void PauseAndUnpause(@Nullable View v) { Button b = (Button) findViewById(R.id.gamePause); LinearLayout l = (LinearLayout) findViewById(R.id.Board); int viz; // Common operations for pause/unpause between modes - blank and unblank board, // start and stop timer, change text on button if (game.IsPaused()) { viz = View.VISIBLE; game.UnpauseTimer(); l.setVisibility(viz); b.setText(R.string.pause); } else { viz = View.INVISIBLE; l.setVisibility(View.INVISIBLE); game.PauseTimer(); b.setText(R.string.unpause); } // Blank and unblank the found sets for normal/Time Attack modes, // blank and unblank the join for PowerSet mode if (game.GetGameType() == GameType.Normal || game.GetGameType() == GameType.TimeAttack) { for (int set = 1; set <= game.GetFullSetCount(); set++) { for (int tile = 1; tile <= 3; tile++) { int id = getResources().getIdentifier(String.format("FoundSet%d_%d", set, tile), "id", getPackageName()); findViewById(id).setVisibility(viz); } } } else { findViewById(R.id.joinImage).setVisibility(viz); } } /** * Gives the user a Tile in a Set on the board they haven't found yet * * @param v The pause button. */ public void GetHint(@Nullable View v) { // For Normal/Time Attack modes, clear all selected Tiles and show the user a Tile from an unfound Set. if (game.IsPaused()) { messageUser(getString(R.string.pausedHints)); } else if (game.GetGameType() == GameType.Normal || game.GetGameType() == GameType.TimeAttack) { ClearTilesSelected(); SetTileSelected(game.GetHint(), true); } else { // Clear an incorrect first set Tile[] foundSet = new Tile[2]; if (selected[0] == null || selected[1] == null || !SetHelper.IsValidSet(selected[0], selected[1], game.GetNextJoin())) { for (int i = 0; i < boardSize; i++) { if (game.GetTileAt(i) == selected[0] || game.GetTileAt(i) == selected[1]) { SetTileSelected(i, false); } } } // Clear an incorrect second set if (selected[2] == null || selected[3] == null || !SetHelper.IsValidSet(selected[2], selected[3], game.GetNextJoin())) { for (int i = 0; i < boardSize; i++) { if (game.GetTileAt(i) == selected[2] || game.GetTileAt(i) == selected[3]) { SetTileSelected(i, false); } } } // If the player has found a valid set, don't get it again for the hint if (selected[0] != null) { foundSet[0] = selected[0]; foundSet[1] = selected[1]; } else if (selected[2] != null) { foundSet[0] = selected[2]; foundSet[1] = selected[3]; } SetTileSelected(game.GetHint(foundSet), true); } } /** * {@inheritDoc} */ @Override protected void onStop() { super.onStop(); if (!game.IsPaused()) { PauseAndUnpause(null); } } /** * {@inheritDoc} */ @Override protected void onRestart() { super.onRestart(); } }