• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
handleMessage(Message msg)110         public void handleMessage(Message msg) {
111             SnakeView.this.update();
112             SnakeView.this.invalidate();
113         }
114 
sleep(long delayMillis)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      */
SnakeView(Context context, AttributeSet attrs)128     public SnakeView(Context context, AttributeSet attrs) {
129         super(context, attrs);
130         initSnakeView();
131    }
132 
SnakeView(Context context, AttributeSet attrs, int defStyle)133     public SnakeView(Context context, AttributeSet attrs, int defStyle) {
134     	super(context, attrs, defStyle);
135     	initSnakeView();
136     }
137 
initSnakeView()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 
initNewGame()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      */
coordArrayListToArray(ArrayList<Coordinate> cvec)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      */
saveState()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      */
coordArrayToArrayList(int[] rawArray)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      */
restoreState(Bundle icicle)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
onKeyDown(int keyCode, KeyEvent msg)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      */
setTextView(TextView newView)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      */
setMode(int newMode)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      */
addRandomApple()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      */
update()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      */
updateWalls()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      */
updateApples()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      */
updateSnake()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 
Coordinate(int newX, int newY)535         public Coordinate(int newX, int newY) {
536             x = newX;
537             y = newY;
538         }
539 
equals(Coordinate other)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
toString()548         public String toString() {
549             return "Coordinate: [" + x + "," + y + "]";
550         }
551     }
552 
553 }
554