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 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