1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.snake; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.os.Bundle; 22 import android.os.Handler; 23 import android.os.Message; 24 import android.util.AttributeSet; 25 import android.util.Log; 26 import android.view.View; 27 import android.widget.TextView; 28 29 import java.util.ArrayList; 30 import java.util.Random; 31 32 /** 33 * SnakeView: implementation of a simple game of Snake 34 */ 35 public class SnakeView extends TileView { 36 37 private static final String TAG = "SnakeView"; 38 39 /** 40 * Current mode of application: READY to run, RUNNING, or you have already lost. static final 41 * ints are used instead of an enum for performance reasons. 42 */ 43 private int mMode = READY; 44 public static final int PAUSE = 0; 45 public static final int READY = 1; 46 public static final int RUNNING = 2; 47 public static final int LOSE = 3; 48 49 /** 50 * Current direction the snake is headed. 51 */ 52 private int mDirection = NORTH; 53 private int mNextDirection = NORTH; 54 private static final int NORTH = 1; 55 private static final int SOUTH = 2; 56 private static final int EAST = 3; 57 private static final int WEST = 4; 58 59 /** 60 * Labels for the drawables that will be loaded into the TileView class 61 */ 62 private static final int RED_STAR = 1; 63 private static final int YELLOW_STAR = 2; 64 private static final int GREEN_STAR = 3; 65 66 /** 67 * mScore: Used to track the number of apples captured mMoveDelay: number of milliseconds 68 * between snake movements. This will decrease as apples are captured. 69 */ 70 private long mScore = 0; 71 private long mMoveDelay = 600; 72 /** 73 * mLastMove: Tracks the absolute time when the snake last moved, and is used to determine if a 74 * move should be made based on mMoveDelay. 75 */ 76 private long mLastMove; 77 78 /** 79 * mStatusText: Text shows to the user in some run states 80 */ 81 private TextView mStatusText; 82 83 /** 84 * mArrowsView: View which shows 4 arrows to signify 4 directions in which the snake can move 85 */ 86 private View mArrowsView; 87 88 /** 89 * mBackgroundView: Background View which shows 4 different colored triangles pressing which 90 * moves the snake 91 */ 92 private View mBackgroundView; 93 94 /** 95 * mSnakeTrail: A list of Coordinates that make up the snake's body mAppleList: The secret 96 * location of the juicy apples the snake craves. 97 */ 98 private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>(); 99 private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>(); 100 101 /** 102 * Everyone needs a little randomness in their life 103 */ 104 private static final Random RNG = new Random(); 105 106 /** 107 * Create a simple handler that we can use to cause animation to happen. We set ourselves as a 108 * target and we can use the sleep() function to cause an update/invalidate to occur at a later 109 * date. 110 */ 111 112 private RefreshHandler mRedrawHandler = new RefreshHandler(); 113 114 class RefreshHandler extends Handler { 115 116 @Override 117 public void handleMessage(Message msg) { 118 SnakeView.this.update(); 119 SnakeView.this.invalidate(); 120 } 121 122 public void sleep(long delayMillis) { 123 this.removeMessages(0); 124 sendMessageDelayed(obtainMessage(0), delayMillis); 125 } 126 }; 127 128 /** 129 * Constructs a SnakeView based on inflation from XML 130 * 131 * @param context 132 * @param attrs 133 */ 134 public SnakeView(Context context, AttributeSet attrs) { 135 super(context, attrs); 136 initSnakeView(context); 137 } 138 139 public SnakeView(Context context, AttributeSet attrs, int defStyle) { 140 super(context, attrs, defStyle); 141 initSnakeView(context); 142 } 143 144 private void initSnakeView(Context context) { 145 146 setFocusable(true); 147 148 Resources r = this.getContext().getResources(); 149 150 resetTiles(4); 151 loadTile(RED_STAR, r.getDrawable(R.drawable.redstar)); 152 loadTile(YELLOW_STAR, r.getDrawable(R.drawable.yellowstar)); 153 loadTile(GREEN_STAR, r.getDrawable(R.drawable.greenstar)); 154 155 } 156 157 private void initNewGame() { 158 mSnakeTrail.clear(); 159 mAppleList.clear(); 160 161 // For now we're just going to load up a short default eastbound snake 162 // that's just turned north 163 164 mSnakeTrail.add(new Coordinate(7, 7)); 165 mSnakeTrail.add(new Coordinate(6, 7)); 166 mSnakeTrail.add(new Coordinate(5, 7)); 167 mSnakeTrail.add(new Coordinate(4, 7)); 168 mSnakeTrail.add(new Coordinate(3, 7)); 169 mSnakeTrail.add(new Coordinate(2, 7)); 170 mNextDirection = NORTH; 171 172 // Two apples to start with 173 addRandomApple(); 174 addRandomApple(); 175 176 mMoveDelay = 600; 177 mScore = 0; 178 } 179 180 /** 181 * Given a ArrayList of coordinates, we need to flatten them into an array of ints before we can 182 * stuff them into a map for flattening and storage. 183 * 184 * @param cvec : a ArrayList of Coordinate objects 185 * @return : a simple array containing the x/y values of the coordinates as 186 * [x1,y1,x2,y2,x3,y3...] 187 */ 188 private int[] coordArrayListToArray(ArrayList<Coordinate> cvec) { 189 int[] rawArray = new int[cvec.size() * 2]; 190 191 int i = 0; 192 for (Coordinate c : cvec) { 193 rawArray[i++] = c.x; 194 rawArray[i++] = c.y; 195 } 196 197 return rawArray; 198 } 199 200 /** 201 * Save game state so that the user does not lose anything if the game process is killed while 202 * we are in the background. 203 * 204 * @return a Bundle with this view's state 205 */ 206 public Bundle saveState() { 207 Bundle map = new Bundle(); 208 209 map.putIntArray("mAppleList", coordArrayListToArray(mAppleList)); 210 map.putInt("mDirection", Integer.valueOf(mDirection)); 211 map.putInt("mNextDirection", Integer.valueOf(mNextDirection)); 212 map.putLong("mMoveDelay", Long.valueOf(mMoveDelay)); 213 map.putLong("mScore", Long.valueOf(mScore)); 214 map.putIntArray("mSnakeTrail", coordArrayListToArray(mSnakeTrail)); 215 216 return map; 217 } 218 219 /** 220 * Given a flattened array of ordinate pairs, we reconstitute them into a ArrayList of 221 * Coordinate objects 222 * 223 * @param rawArray : [x1,y1,x2,y2,...] 224 * @return a ArrayList of Coordinates 225 */ 226 private ArrayList<Coordinate> coordArrayToArrayList(int[] rawArray) { 227 ArrayList<Coordinate> coordArrayList = new ArrayList<Coordinate>(); 228 229 int coordCount = rawArray.length; 230 for (int index = 0; index < coordCount; index += 2) { 231 Coordinate c = new Coordinate(rawArray[index], rawArray[index + 1]); 232 coordArrayList.add(c); 233 } 234 return coordArrayList; 235 } 236 237 /** 238 * Restore game state if our process is being relaunched 239 * 240 * @param icicle a Bundle containing the game state 241 */ 242 public void restoreState(Bundle icicle) { 243 setMode(PAUSE); 244 245 mAppleList = coordArrayToArrayList(icicle.getIntArray("mAppleList")); 246 mDirection = icicle.getInt("mDirection"); 247 mNextDirection = icicle.getInt("mNextDirection"); 248 mMoveDelay = icicle.getLong("mMoveDelay"); 249 mScore = icicle.getLong("mScore"); 250 mSnakeTrail = coordArrayToArrayList(icicle.getIntArray("mSnakeTrail")); 251 } 252 253 /** 254 * Handles snake movement triggers from Snake Activity and moves the snake accordingly. Ignore 255 * events that would cause the snake to immediately turn back on itself. 256 * 257 * @param direction The desired direction of movement 258 */ 259 public void moveSnake(int direction) { 260 261 if (direction == Snake.MOVE_UP) { 262 if (mMode == READY | mMode == LOSE) { 263 /* 264 * At the beginning of the game, or the end of a previous one, 265 * we should start a new game if UP key is clicked. 266 */ 267 initNewGame(); 268 setMode(RUNNING); 269 update(); 270 return; 271 } 272 273 if (mMode == PAUSE) { 274 /* 275 * If the game is merely paused, we should just continue where we left off. 276 */ 277 setMode(RUNNING); 278 update(); 279 return; 280 } 281 282 if (mDirection != SOUTH) { 283 mNextDirection = NORTH; 284 } 285 return; 286 } 287 288 if (direction == Snake.MOVE_DOWN) { 289 if (mDirection != NORTH) { 290 mNextDirection = SOUTH; 291 } 292 return; 293 } 294 295 if (direction == Snake.MOVE_LEFT) { 296 if (mDirection != EAST) { 297 mNextDirection = WEST; 298 } 299 return; 300 } 301 302 if (direction == Snake.MOVE_RIGHT) { 303 if (mDirection != WEST) { 304 mNextDirection = EAST; 305 } 306 return; 307 } 308 309 } 310 311 /** 312 * Sets the Dependent views that will be used to give information (such as "Game Over" to the 313 * user and also to handle touch events for making movements 314 * 315 * @param newView 316 */ 317 public void setDependentViews(TextView msgView, View arrowView, View backgroundView) { 318 mStatusText = msgView; 319 mArrowsView = arrowView; 320 mBackgroundView = backgroundView; 321 } 322 323 /** 324 * Updates the current mode of the application (RUNNING or PAUSED or the like) as well as sets 325 * the visibility of textview for notification 326 * 327 * @param newMode 328 */ 329 public void setMode(int newMode) { 330 int oldMode = mMode; 331 mMode = newMode; 332 333 if (newMode == RUNNING && oldMode != RUNNING) { 334 // hide the game instructions 335 mStatusText.setVisibility(View.INVISIBLE); 336 update(); 337 // make the background and arrows visible as soon the snake starts moving 338 mArrowsView.setVisibility(View.VISIBLE); 339 mBackgroundView.setVisibility(View.VISIBLE); 340 return; 341 } 342 343 Resources res = getContext().getResources(); 344 CharSequence str = ""; 345 if (newMode == PAUSE) { 346 mArrowsView.setVisibility(View.GONE); 347 mBackgroundView.setVisibility(View.GONE); 348 str = res.getText(R.string.mode_pause); 349 } 350 if (newMode == READY) { 351 mArrowsView.setVisibility(View.GONE); 352 mBackgroundView.setVisibility(View.GONE); 353 354 str = res.getText(R.string.mode_ready); 355 } 356 if (newMode == LOSE) { 357 mArrowsView.setVisibility(View.GONE); 358 mBackgroundView.setVisibility(View.GONE); 359 str = res.getString(R.string.mode_lose, mScore); 360 } 361 362 mStatusText.setText(str); 363 mStatusText.setVisibility(View.VISIBLE); 364 } 365 366 /** 367 * @return the Game state as Running, Ready, Paused, Lose 368 */ 369 public int getGameState() { 370 return mMode; 371 } 372 373 /** 374 * Selects a random location within the garden that is not currently covered by the snake. 375 * Currently _could_ go into an infinite loop if the snake currently fills the garden, but we'll 376 * leave discovery of this prize to a truly excellent snake-player. 377 */ 378 private void addRandomApple() { 379 Coordinate newCoord = null; 380 boolean found = false; 381 while (!found) { 382 // Choose a new location for our apple 383 int newX = 1 + RNG.nextInt(mXTileCount - 2); 384 int newY = 1 + RNG.nextInt(mYTileCount - 2); 385 newCoord = new Coordinate(newX, newY); 386 387 // Make sure it's not already under the snake 388 boolean collision = false; 389 int snakelength = mSnakeTrail.size(); 390 for (int index = 0; index < snakelength; index++) { 391 if (mSnakeTrail.get(index).equals(newCoord)) { 392 collision = true; 393 } 394 } 395 // if we're here and there's been no collision, then we have 396 // a good location for an apple. Otherwise, we'll circle back 397 // and try again 398 found = !collision; 399 } 400 if (newCoord == null) { 401 Log.e(TAG, "Somehow ended up with a null newCoord!"); 402 } 403 mAppleList.add(newCoord); 404 } 405 406 /** 407 * Handles the basic update loop, checking to see if we are in the running state, determining if 408 * a move should be made, updating the snake's location. 409 */ 410 public void update() { 411 if (mMode == RUNNING) { 412 long now = System.currentTimeMillis(); 413 414 if (now - mLastMove > mMoveDelay) { 415 clearTiles(); 416 updateWalls(); 417 updateSnake(); 418 updateApples(); 419 mLastMove = now; 420 } 421 mRedrawHandler.sleep(mMoveDelay); 422 } 423 424 } 425 426 /** 427 * Draws some walls. 428 */ 429 private void updateWalls() { 430 for (int x = 0; x < mXTileCount; x++) { 431 setTile(GREEN_STAR, x, 0); 432 setTile(GREEN_STAR, x, mYTileCount - 1); 433 } 434 for (int y = 1; y < mYTileCount - 1; y++) { 435 setTile(GREEN_STAR, 0, y); 436 setTile(GREEN_STAR, mXTileCount - 1, y); 437 } 438 } 439 440 /** 441 * Draws some apples. 442 */ 443 private void updateApples() { 444 for (Coordinate c : mAppleList) { 445 setTile(YELLOW_STAR, c.x, c.y); 446 } 447 } 448 449 /** 450 * Figure out which way the snake is going, see if he's run into anything (the walls, himself, 451 * or an apple). If he's not going to die, we then add to the front and subtract from the rear 452 * in order to simulate motion. If we want to grow him, we don't subtract from the rear. 453 */ 454 private void updateSnake() { 455 boolean growSnake = false; 456 457 // Grab the snake by the head 458 Coordinate head = mSnakeTrail.get(0); 459 Coordinate newHead = new Coordinate(1, 1); 460 461 mDirection = mNextDirection; 462 463 switch (mDirection) { 464 case EAST: { 465 newHead = new Coordinate(head.x + 1, head.y); 466 break; 467 } 468 case WEST: { 469 newHead = new Coordinate(head.x - 1, head.y); 470 break; 471 } 472 case NORTH: { 473 newHead = new Coordinate(head.x, head.y - 1); 474 break; 475 } 476 case SOUTH: { 477 newHead = new Coordinate(head.x, head.y + 1); 478 break; 479 } 480 } 481 482 // Collision detection 483 // For now we have a 1-square wall around the entire arena 484 if ((newHead.x < 1) || (newHead.y < 1) || (newHead.x > mXTileCount - 2) 485 || (newHead.y > mYTileCount - 2)) { 486 setMode(LOSE); 487 return; 488 489 } 490 491 // Look for collisions with itself 492 int snakelength = mSnakeTrail.size(); 493 for (int snakeindex = 0; snakeindex < snakelength; snakeindex++) { 494 Coordinate c = mSnakeTrail.get(snakeindex); 495 if (c.equals(newHead)) { 496 setMode(LOSE); 497 return; 498 } 499 } 500 501 // Look for apples 502 int applecount = mAppleList.size(); 503 for (int appleindex = 0; appleindex < applecount; appleindex++) { 504 Coordinate c = mAppleList.get(appleindex); 505 if (c.equals(newHead)) { 506 mAppleList.remove(c); 507 addRandomApple(); 508 509 mScore++; 510 mMoveDelay *= 0.9; 511 512 growSnake = true; 513 } 514 } 515 516 // push a new head onto the ArrayList and pull off the tail 517 mSnakeTrail.add(0, newHead); 518 // except if we want the snake to grow 519 if (!growSnake) { 520 mSnakeTrail.remove(mSnakeTrail.size() - 1); 521 } 522 523 int index = 0; 524 for (Coordinate c : mSnakeTrail) { 525 if (index == 0) { 526 setTile(YELLOW_STAR, c.x, c.y); 527 } else { 528 setTile(RED_STAR, c.x, c.y); 529 } 530 index++; 531 } 532 533 } 534 535 /** 536 * Simple class containing two integer values and a comparison function. There's probably 537 * something I should use instead, but this was quick and easy to build. 538 */ 539 private class Coordinate { 540 public int x; 541 public int y; 542 543 public Coordinate(int newX, int newY) { 544 x = newX; 545 y = newY; 546 } 547 548 public boolean equals(Coordinate other) { 549 if (x == other.x && y == other.y) { 550 return true; 551 } 552 return false; 553 } 554 555 @Override 556 public String toString() { 557 return "Coordinate: [" + x + "," + y + "]"; 558 } 559 } 560 561 } 562