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