Home | History | Annotate | Download | only in snake
      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