Download Android Pig Development Tutorial - Gettysburg College Computer

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts
no text concepts found
Transcript
Android Pig Development Tutorial
Todd Neller, Gettysburg College, October 13th, 2012
Before we begin developing, we first introduce the game we’ll be developing.1 The game of Pig2 is a
simple jeopardy dice game that excels as a teaching tool because it has very simple rules while still being
fun to play. It thus has a high fun-to-SLOC (Source Lines Of Code) ratio. We can state the rules in two
sentences:
The first player to score 100 or more points wins. On a player’s turn, the player rolls the die as
many times as desired until either (1) the player “holds” (i.e. chooses to stop rolling) and scores
the sum of the rolls, or (2) the player rolls a 1 (“pig”) and scores nothing that turn.
For example, suppose Ann has 20 points. Ann rolls a 6 and has a turn total of 6 points. Ann can either
hold, score 6 points, and end the turn with 20 + 6 = 26 points, or Ann can keep rolling. Ann rolls a 2, and
can either hold, scoring 6 + 2 = 8 points (bringing her score to 28), or can keep rolling. Ann chooses to
roll again, and rolls a 1 (“pig”), so she scores no points for the turn, but still retains her 20 points from
previous turn(s). Her turn is now over.
The key pieces of information for decision-making in the game are the player’s scores and the current
turn total. At any time, the decision is whether the current player wishes to roll or hold. This makes for
a very simple game implementation exercise that allows us to become familiar with labels (for game
information), images (for displaying the die), and buttons (for choosing to roll/hold). Now we turn our
attention to developing Pig. The following tutorial assumes one is using Eclipse with the Android SDK
already installed (http://developer.android.com/sdk/installing.html). It also assumes that one regularly
updates the packages imported using the handy control-shift-o feature of Eclipse.
On our system, one specifies the location of the Android SDK via Window  Preferences  Android,
setting the path to “/usr/local/android-sdk-linux/”.
In Eclipse, we need to first create a new Android Virtual Device (AVD) for emulation of your program.





Window AVD Manager  New…
Name: AVD1.5 (can name this anything intuitive to you)
Target: Android 1.5 - API Level 3
Click “Create AVD”.
Close Android AVD Manager window.
Next, we create a new Android project that starts with a sort of “Hello, world!” do-nothing app skeleton.
1
http://cs.gettysburg.edu/~tneller/resources/pig/cs1/gui.html
2
http://cs.gettysburg.edu/projects/pig/



File  New  Android Application Project
Call application name “Pig”, the project “Android Pig”, and the package name
“edu.gettysburg.pig”. Select the Build SDK to be “Android 1.5 (API 3)”, select the Minimum
Required SDK to “API 3: Android 1.5 (Cupcake)”
Click the “Next>” button 3 times, and click the “Finish” button.
Open your new Android Pig project in the package explorer and poke around in the subfolders. Note:




In src (source files) edu.gettysburg.pig is the main Java file you’ll be programming:
MainActivity.java
In res (resource files) drawable is where you will place image files.
In res  layout  main.xml is the XML (eXtensible Markup Language) file defining the main
layout for your app.
In res  values  strings.xml is the XML file defining the strings used in your app.
NOTE: In our installation, you will start with an error in your project. To correct the error, open the
AndroidManifest.xml, select the AndroidManifest.xml tab in the bottom-center, delete
android:targetSdkVersion = “15”, and save.
For now, open MainActivity.java and click the run button. Run it as an “Android Applicaton”. The
emulator will take a long time to start, but unless you close it or get it into a funky state, you’ll not need
to restart it between debugging runs. Control-<Window>-F113 toggles the orientation between portrait
and landscape. The “Hello World!” app runs automatically in the emulator window.
As a first step towards creating an app, we need to populate it with resources,


Copy the provided Pig app image files4 into the res/drawable subdirectory of your project.
Press F5 to refresh your project contents and show that the files are in place.
Next, open res/values/strings.xml. Note the bottom tabs that allow you to toggle between a form-based
editor, and direct editing of XML. We’ll use the form editor and then see the generated XML. “Add…”
the following elements types with resource names and values:
Resource
Type
String
String
String
String
String
String
Color
3
4
Name
your_score
my_score
turn_total
roll
hold
zero
black
Value
Your Score:
My Score:
Turn Total:
Roll
Hold
0
#000000
On some systems, this will be Control-Alt-F11 or just simply Control-F11.
http://cs.gettysburg.edu/~tneller/resources/pig/cs1/images/png/
Color
white
#ffffff
Now choose the strings.xml lower tab and change the app_name and title_activity_main to “Pig”. It
should look something like this:
<resources>
<string name="app_name">Android Pig 2012</string>
<string name="hello_world">Hello world!</string>
<string name="menu_settings">Settings</string>
<string name="title_activity_main">Pig</string>
<string name="your_score">Your Score:</string>
<string name="my_score">My Score:</string>
<string name="turn_total">Turn Total:</string>
<string name="roll">Roll</string>
<string name="hold">Hold</string>
<string name="zero">0</string>
<color name="black">#000000</color>
<color name="white">#ffffff</color>
</resources>
Why not just use the Strings in the app program? The reason for such basic specification of resources is
that it makes internationalization easier. All we need to do in order to create a version for another
language is to create a resource subdirectory with a language code (e.g. res/values-es/ for Spanish),
and reuse the XML with the same names but with translated values. Similarly, we can specify different
images for different resolutions by having different image subdirectories. It’s beyond the scope of this
tutorial, but it’s motivating and good to know.
Next, we create a layout to hold these elements. Open res/layout/activity_main.xml. Here too we can
toggle between a graphical editor and direct XML specification. If something seems awry in the
graphical editor, look at the generated XML and it’s outline form to the right to see what has happened.
We’d like to create a layout that looks like this:
NOTE: At time of writing, the Graphical Layout pane is not faithfully representing the layout as shown in
the images of this document. One must run and test the app to see its true appearance. Thus, we’ll rely
heavily on the Outline pane to do the bulk of our editing. The direct xml editing pane is also helpful for
clarifying details.
In the Outline pane, right-click and “Remove” the RelativeLayout to start fresh. From the Graphical
Layout pane, drag a Layouts  LinearLayout (Vertical) element into the center of the Graphical pane.5
Next, we’ll drag three elements from the Graphical Layout pane into the LinearLayout element of the
Outline pane:



Layouts TableLayout
Images & Media  ImageView (Choose the “roll” image if prompted.)
Layouts TableLayout
Place each at the bottom of previous elements.
Next select the tableLayout1 element from the Outline pane and notice how it expands and highlights in
the Graphical Layout window in the center. If there are not already TableRows in each table layout, drag
three Layout  TableRow elements to the first TableLayout, and one more to the second, such that they
appear approximately as below:
Delete any extra unwanted TableRow elements. Next, we’ll add two Form Widgets  TextView
elements to each of the first three table rows. Also, we’ll add two Form Widgets  Button elements to
the bottom fourth table row. The Outline should now look approximately like this:
5
NOTE: In our installation this defaults to the creation of a LinearLayout (Horizontal)! Click the “Set Vertical
Orientation” button in the upper-left of the Graphical Layout pane to correct this.
For now, don’t worry if the names don’t match those shown here. The Graphical Layout pane should
look something like this:
Next, it would be nice to center these elements and expand table row elements to fill the width of the
screen. Now that we have the elements in place, we’ll see what changing some of their Properties can
do. By right-clicking on an element in the Graphical Layout or Outline panes, we can choose
“Properties”. For example, in the Outline Layout, right-click the top-level LinearLayout, choose Other
Properties  Inherited from View  Background…  Color  “black”. This “black” is the color we
defined in strings.xml.
Next, to make sure the TextView labels are legible, select all of them by clicking one in the Outline
panes, and Control-clicking the rest. Then, right-click on one and select Edit TextColor… and set it to
“white”. Note that the property changes could also be made through the lower-right Properties pane.
Select all 6 TextView and 2 Button elements in the Outline window by Control-clicking each. Then rightclick, choose Other Properties  Layout Parameters  Layout Weight…, and enter the number 1.
To Center the image, select the ImageView, Other Properties  Layout Parameters  Layout Gravity 
Center.
Select all three left column TextView elements, and choose Other Properties  Defined by TextView 
Layout Gravity  Right. We want to have spacing between the left and right TextView elements, so
with the same three, choose Other Properties  Inherited from View  PaddingRight…, and enter
“4pt”. At this point, your Graphical Layout should look something like this:
Finally, it’s time to change element names for more intuitive programming, and fill each with the
resources we’ve defined. Here is a list of the changes you’ll now make to each:









textView1: Edit Text…  your_score
textView3: Edit Text…  my_score
textView5: Edit Text…  turn_total
textView2: Edit Text…  zero, Edit ID…  textViewYourScore
textView4: Edit Text…  zero, Edit ID…  textViewMyScore
textView6: Edit Text…  zero, Edit ID…  textViewTurnTotal
imageView1: Properties  src…  drawable  roll, Edit ID…  imageView
button1: Edit Text…  roll, Edit ID…  buttonRoll
button2: Edit Text…  hold, Edit ID…  buttonHold
Finally, select all elements in the Outline window, right-click and choose Layout Width  Fill Parent.
This will affect 13 out of the 16 elements.
Now, your Graphical Layout and Outline should look like this:
And the main.xml should look something like this:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/LinearLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@color/black"
android:orientation="vertical" >
<TableLayout
android:id="@+id/tableLayout1"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TableRow
android:id="@+id/tableRow1"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/textView1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_weight="1"
android:gravity="right"
android:paddingRight="4pt"
android:text="@string/your_score"
android:textColor="@color/white" />
<TextView
android:id="@+id/textViewYourScore"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/zero"
android:textColor="@color/white" />
</TableRow>
<TableRow
android:id="@+id/tableRow2"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/textView3"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_weight="1"
android:gravity="right"
android:paddingRight="4pt"
android:text="@string/my_score"
android:textColor="@color/white" />
<TextView
android:id="@+id/textViewMyScore"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/zero"
android:textColor="@color/white" />
</TableRow>
<TableRow
android:id="@+id/tableRow3"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/textView5"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_weight="1"
android:gravity="right"
android:paddingRight="4pt"
android:text="@string/turn_total"
android:textColor="@color/white" />
<TextView
android:id="@+id/textViewTurnTotal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/zero"
android:textColor="@color/white" />
</TableRow>
</TableLayout>
<ImageView
android:id="@+id/imageView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/roll" />
<TableLayout
android:id="@+id/tableLayout2"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TableRow
android:id="@+id/tableRow4"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<Button
android:id="@+id/buttonRoll"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/roll" />
<Button
android:id="@+id/buttonHold"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/hold" />
</TableRow>
</TableLayout>
</LinearLayout>
One can make many improvements to this layout, but this suffices for our demonstration purposes. Do
a test run to see that all looks good in emulation. Now that we’ve laid the groundwork, let’s turn our
attention to the code that gives life to the interface. We’ll approach the project in stages:
1. Define variables and bind them to resources and GUI elements.
2. Set up means to update our TextView labels and ImageView image, testing it with a simple
behavior: die rolling with appropriate turn total updates.
3. Add a hold action that adds the turn total to the current player’s score, and resets the turn total
to zero
4. Add a turn changing behavior that changes the current player.
5. Introduce a computer player, disabling buttons when the computer is playing, and showing how
one can interact with the GUI thread from another thread.
6. Detect when a player wins, report the win, and ask the user whether to play again or not.
7. Show how to store and restore state during interruptions to the app, e.g. when the display
orientation changes.
First, add the following fields to the MainActivity class. These set up constants, variables for game state,
and references to GUI elements and resources.
// COMPUTER_DELAY - delay between computer rolls in milliseconds
protected static final long COMPUTER_DELAY = 1000;
// GOAL_SCORE - goal score at or above which the holding player wins
private static final int GOAL_SCORE = 100;
// Game state variables:
private int userScore = 0, computerScore = 0, turnTotal = 0;
// userStartGame - whether or not the user starts the current game
private boolean userStartGame = true;
// isUserTurn - whether or not it is currently the user's turn
private boolean isUserTurn = true;
// imageName - name of the current displayed image
private String imageName = "roll";
// GUI views
private TextView textViewYourScore, textViewMyScore, textViewTurnTotal;
private ImageView imageView;
// GUI buttons
private Button buttonRoll, buttonHold;
// mapping from image strings to Drawable resources
private HashMap<String, Drawable> drawableMap = new HashMap<String, Drawable>();
// random - random number generator for rolling dice
private Random random;
Next, we initialize these in the onCreate method, adding the following lines within the default
implementation:
textViewYourScore = (TextView) findViewById(R.id.textViewYourScore);
textViewMyScore = (TextView) findViewById(R.id.textViewMyScore);
textViewTurnTotal = (TextView) findViewById(R.id.textViewTurnTotal);
buttonRoll = (Button) findViewById(R.id.buttonRoll);
buttonHold = (Button) findViewById(R.id.buttonHold);
imageView = (ImageView) findViewById(R.id.imageView);
drawableMap.put("roll", getResources().getDrawable(R.drawable.roll));
drawableMap.put("hold", getResources().getDrawable(R.drawable.hold));
drawableMap.put("die1", getResources().getDrawable(R.drawable.die1));
drawableMap.put("die2", getResources().getDrawable(R.drawable.die2));
drawableMap.put("die3", getResources().getDrawable(R.drawable.die3));
drawableMap.put("die4", getResources().getDrawable(R.drawable.die4));
drawableMap.put("die5", getResources().getDrawable(R.drawable.die5));
drawableMap.put("die6", getResources().getDrawable(R.drawable.die6));
random = new Random();
When we want to get a reference to a GUI element, we use findViewById and find the element
using a constant named by the GUI element ID we defined within a class called R. For example, we get
our roll button using findViewById(R.id.buttonRoll), which then must be cast to a Button.
The resource class R you see used frequently is auto-generated from our XML specifications. R.java
should never be edited directly. To get resources we use the inherited method getResources().
More specifically, to get a Drawable image resources, we use:
getResources().getDrawable(R.drawable.<insert ID here>).
The hash map drawableMap is set up to allow convenient reference to our images by mapping simple
strings to the associated Drawable resources we retrieve. Think of this as being like an array of
Drawable resource indexed by Strings. Finally, we create our random number generator.
It would be easy to update a score variable and forget to change the corresponding label (or vice versa),
so it’s often good practice to create methods to perform such changes at the same time, keeping
information consistent. We will now create such methods to update our views. Add the following
methods:
private void setUserScore(final int newScore) {
userScore = newScore;
textViewYourScore.setText(String.valueOf(newScore));
}
private void setComputerScore(final int newScore) {
computerScore = newScore;
textViewMyScore.setText(String.valueOf(newScore));
}
private void setTurnTotal(final int newTotal) {
turnTotal = newTotal;
textViewTurnTotal.setText(String.valueOf(newTotal));
}
private void setImage(final String newImageName) {
imageName = newImageName;
imageView.setImageDrawable(drawableMap.get(imageName));
}
Each of these takes a piece of information about the state of the game or current image, stores it in the
relevant field, and causes the GUI view we reference to update accordingly. Note that we need to
convert the integers to text with String.valueOf, and we use the drawableMap to easily retrieve
a specified image.
To test this, we need to add our first simple user interaction. Add the following code to the end of
method onCreate in order to cause a click of our roll and hold buttons to call methods roll() and
hold() , respectively:
buttonRoll.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
roll();
}
});
buttonHold.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
hold();
}
});
Accordingly, create private roll and hold methods:
private void roll() {
}
private void hold() {
}
In hold, we wish to first take the simple step of rolling a die and changing the image of the associated
die. We can do so as follows:
private void roll() {
int roll = random.nextInt(6) + 1;
setImage("die" + roll);
}
Test it. Now, let’s update the turn total, setting it to 0 when the roll is a 1, and accumulating the roll to
the turn total otherwise:
private void roll() {
int roll = random.nextInt(6) + 1;
setImage("die" + roll);
if (roll == 1) {
setTurnTotal(0);
}
else {
setTurnTotal(turnTotal + roll);
}
}
Test. For the hold method, we want to set the image to the “hold” image, accumulate the turn total to
the current player’s score and reset the turn total to 0:
private void hold() {
setImage("hold");
if (isUserTurn)
setUserScore(userScore + turnTotal);
else
setComputerScore(computerScore + turnTotal);
setTurnTotal(0);
}
Test. At this point, we want to add the ability to change whose turn it is. For this, we add a new
method, changeTurn() and call it at the appropriate points in roll() and hold().
private void roll() {
int roll = random.nextInt(6) + 1;
setImage("die" + roll);
if (roll == 1) {
setTurnTotal(0);
changeTurn();
}
else {
setTurnTotal(turnTotal + roll);
}
}
private void hold() {
setImage("hold");
if (isUserTurn)
setUserScore(userScore + turnTotal);
else
setComputerScore(computerScore + turnTotal);
setTurnTotal(0);
changeTurn();
}
private void changeTurn() {
isUserTurn = !isUserTurn;
}
Test. We next wish to add a computer player. The strategy this computer player will follow was
devised by the author and Clif Presser and is called the “Keep Pace and End Race” strategy6. While not
optimal, it is within 1% of optimal performance and makes for a challenging computer player. The
strategy is as follows:



If the player can hold and win, hold.
Otherwise, if either player has a score 71 or higher, keep rolling until the goal is reached.
Otherwise, subtract the player’s score from the opponent’s score, divide by 8, round to the
nearest integer, add 21, and use the result as the turn total at or above which the player should
hold.
To implement this, we need to create a separate thread of execution, where the computer delays
between decisions, allowing the human opponent to follow the computer’s turn progress. (We also
want to disable the buttons during the computer turn, but we’ll do this later.) However, we must be
careful when calling methods on the GUI thread from another thread. If we try to interact directly with
the GUI from another thread of execution, it will result in an application crash. Below, we can see the
great care that must be taken to queue-up method calls for the GUI thread in a way that is thread-safe.
Observe and imitate the patterns here:
private void computerTurn() {
new Thread(new Runnable() {
public void run() {
Thread.yield();
try { Thread.sleep(COMPUTER_DELAY); }
catch (InterruptedException e) { e.printStackTrace(); }
while (!isUserTurn) {
int holdValue = 21 + (int) Math.round((userScore - computerScore)/8.0);
if (!(computerScore + turnTotal >= GOAL_SCORE) &&
(userScore >= 71 || computerScore >= 71 || turnTotal < holdValue))
runOnUiThread(new Runnable() {public void run() {roll();}});
else {
runOnUiThread(new Runnable() {public void run() {hold();}});
break;
}
Thread.yield();
6
Practical Play of the Dice Game Pig, The UMAP Journal 31(1) (2010), pp. 5-19.
try { Thread.sleep(COMPUTER_DELAY); }
catch (InterruptedException e) { e.printStackTrace(); }
}
}
}).start();
}
In addition to common use of Java Threads (beyond the score of this tutorial), especially note the use of
the runOnUiThread method, which queues-up a Runnable object that the GUI thread itself will
invoke when it is safe to invoke. We would naturally invoke this computerTurn method in the
changeTurn method:
private void changeTurn() {
isUserTurn = !isUserTurn;
if (!isUserTurn)
computerTurn();
}
Test. Naturally, we’d like to make it so that the user can’t click the buttons and interfere with the
computer’s turn. First, we create a method setButtonsState that makes sure that buttons are
enabled or disabled according to which player is currently playing:
private void setButtonsState() {
buttonHold.setEnabled(isUserTurn);
buttonRoll.setEnabled(isUserTurn);
}
Further, we call this in the changeTurn method:
private void changeTurn() {
isUserTurn = !isUserTurn;
setButtonsState();
if (!isUserTurn)
computerTurn();
}
Test. Next, we would like to detect a game winning condition, and create a popup window that
announces the win and asks the player whether or not another game is desired. If so, the starting player
changes, and the game is reset to initial conditions. If not, the app exits. This is accomplished in the
following endGame method:
private void endGame() {
String message = (!isUserTurn)
? String.format("I win %d to %d.", computerScore, userScore)
: String.format("You win %d to %d.", userScore, computerScore);
message += " Would you like to play again?";
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(message)
.setCancelable(false)
.setPositiveButton("New Game", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
setUserScore(0);
setComputerScore(0);
setTurnTotal(0);
userStartGame = !userStartGame;
isUserTurn = userStartGame;
setButtonsState();
if (isUserTurn)
setImage("roll");
else
computerTurn();
dialog.cancel();
}
})
.setNegativeButton("Quit", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
MainActivity.this.finish();
}
});
AlertDialog alert = builder.create();
alert.show();
}
There is a lot going on here. In the first lines, we build up the message using String formatting and the
Java selection operator – just standard Java with no Android particulars. Everything else is based on the
particulars of Android’s AlertDialog class. The AlertDialog.Builder allows a chain of
method calls where it returns itself each time for further modification. We set the message, disable
cancellation of the dialog, and set up the behaviors of the positive and negative answer buttons, which
we label “New Game” and “Quit”, respectively.
The positive “New Game” button, when clicked, causes the game state to be reset, the starting player to
change, the current player to be set to the starting player, button states to be updated, the image reset
or the computer player set in motion as appropriate, and the popup dialog to close.
The negative “Quit” button simply terminates the app. Now that the popup alert dialog has been
specified, it is created, and we show it.
We test for the end game condition in the hold method:
private void hold() {
setImage("hold");
if (isUserTurn)
setUserScore(userScore + turnTotal);
else
setComputerScore(computerScore + turnTotal);
setTurnTotal(0);
if (userScore >= GOAL_SCORE || computerScore >= GOAL_SCORE)
endGame();
else
changeTurn();
}
To test the game ending condition easily, I recommend temporarily changing GOAL_SCORE to 20. Do
so, and test your app to make sure it functions correctly. Then change it back to 100.
We now reach the last stage, where we equip our app to gracefully handle interruptions. A simple
example of an interruption to execution occurs when the Android phone is rotated and the screen
orientation changes. You can do this in emulation by typing control-F11. Try playing a game for a bit
until there’s a score, and then type control-F11.
This causes the app to completely reinitialize. If we want to regain our previous state, then we need to
save it in what is called the app Bundle. Now one can see that all of the state variables have an
additional purpose: to store and restore an app’s state.
To store out an app’s state when interrupted by a call, reorientation, etc., we need to add an
onSaveInstanceState method like the following:
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("userScore", userScore);
outState.putInt("computerScore", computerScore);
outState.putInt("turnTotal", turnTotal);
outState.putBoolean("userStartGame", userStartGame);
outState.putBoolean("isUserTurn", isUserTurn);
outState.putString("imageName", imageName);
}
Each essential pieces of information is stored in a Bundle object. We give them arbitrary labels.
Labels that match the corresponding variables are intuitive choices. Next, we add an
onRestoreInstanceState method that does the reverse and sets in motion what was previously
happening:
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
setUserScore(savedInstanceState.getInt("userScore", 0));
setComputerScore(savedInstanceState.getInt("computerScore", 0));
setTurnTotal(savedInstanceState.getInt("turnTotal", 0));
setImage(savedInstanceState.getString("imageName"));
userStartGame = savedInstanceState.getBoolean("userStartGame", true);
isUserTurn = savedInstanceState.getBoolean("isUserTurn", true);
setButtonsState();
if (userScore >= GOAL_SCORE || computerScore >= GOAL_SCORE)
endGame();
else if (!isUserTurn)
computerTurn();
}
Note a few important things here: The getInt and getBoolean methods include default values.
Also, note that we have to think through every possible case in the last lines. Are the buttons
enabled/disabled? Was there a game end popup at the time? Is it currently the computer’s turn, such
that I need to restart that thread?
Coding an app takes care, good coding discipline, and the building of good habits. You’ll find some
things easier/harder than expected. Constraints will force you to change your style of programming.
For example, use of threads is very important to ensure that the app is always responsive to user input.
Even a fraction of a second where a button press is being ignored causes Android to force close an app.
Immediate responsiveness is key, and that dictates a different style of coding.
At this point, we’ve reach the goal and your code should look something like this:
package edu.gettysburg.pig;
import
import
import
import
import
import
import
import
import
import
import
java.util.HashMap;
java.util.Random;
android.app.Activity;
android.app.AlertDialog;
android.content.DialogInterface;
android.graphics.drawable.Drawable;
android.os.Bundle;
android.view.View;
android.widget.Button;
android.widget.ImageView;
android.widget.TextView;
public class MainActivity extends Activity {
// COMPUTER_DELAY - delay between computer rolls in milliseconds
protected static final long COMPUTER_DELAY = 1000;
// GOAL_SCORE - goal score at or above which the holding player wins
private static final int GOAL_SCORE = 100;
// Game state variables:
private int userScore = 0, computerScore = 0, turnTotal = 0;
// userStartGame - whether or not the user starts the current game
private boolean userStartGame = true;
// isUserTurn - whether or not it is currently the user's turn
private boolean isUserTurn = true;
// imageName - name of the current displayed image
private String imageName = "roll";
// GUI views
private TextView textViewYourScore, textViewMyScore, textViewTurnTotal;
private ImageView imageView;
// GUI buttons
private Button buttonRoll, buttonHold;
// mapping from image strings to Drawable resources
private HashMap<String, Drawable> drawableMap = new HashMap<String, Drawable>();
// random - random number generator for rolling dice
private Random random;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
textViewYourScore = (TextView) findViewById(R.id.textViewYourScore);
textViewMyScore = (TextView) findViewById(R.id.textViewMyScore);
textViewTurnTotal = (TextView) findViewById(R.id.textViewTurnTotal);
buttonRoll = (Button) findViewById(R.id.buttonRoll);
buttonHold = (Button) findViewById(R.id.buttonHold);
imageView = (ImageView) findViewById(R.id.imageView);
drawableMap.put("roll", getResources().getDrawable(R.drawable.roll));
drawableMap.put("hold", getResources().getDrawable(R.drawable.hold));
drawableMap.put("die1", getResources().getDrawable(R.drawable.die1));
drawableMap.put("die2", getResources().getDrawable(R.drawable.die2));
drawableMap.put("die3", getResources().getDrawable(R.drawable.die3));
drawableMap.put("die4", getResources().getDrawable(R.drawable.die4));
drawableMap.put("die5", getResources().getDrawable(R.drawable.die5));
drawableMap.put("die6", getResources().getDrawable(R.drawable.die6));
random = new Random();
buttonRoll.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
roll();
}
});
buttonHold.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
hold();
}
});
}
private void setUserScore(final int newScore) {
userScore = newScore;
textViewYourScore.setText(String.valueOf(newScore));
}
private void setComputerScore(final int newScore) {
computerScore = newScore;
textViewMyScore.setText(String.valueOf(newScore));
}
private void setTurnTotal(final int newTotal) {
turnTotal = newTotal;
textViewTurnTotal.setText(String.valueOf(newTotal));
}
private void setImage(final String newImageName) {
imageName = newImageName;
imageView.setImageDrawable(drawableMap.get(imageName));
}
private void roll() {
int roll = random.nextInt(6) + 1;
setImage("die" + roll);
if (roll == 1) {
setTurnTotal(0);
changeTurn();
}
else {
setTurnTotal(turnTotal + roll);
}
}
private void hold() {
setImage("hold");
if (isUserTurn)
setUserScore(userScore + turnTotal);
else
setComputerScore(computerScore + turnTotal);
setTurnTotal(0);
if (userScore >= GOAL_SCORE || computerScore >= GOAL_SCORE)
endGame();
else
changeTurn();
}
private void changeTurn() {
isUserTurn = !isUserTurn;
setButtonsState();
if (!isUserTurn)
computerTurn();
}
private void computerTurn() {
new Thread(new Runnable() {
public void run() {
Thread.yield();
try { Thread.sleep(COMPUTER_DELAY); }
catch (InterruptedException e) { e.printStackTrace(); }
while (!isUserTurn) {
int holdValue = 21 + (int) Math.round((userScore - computerScore)/8.0);
if (!(computerScore + turnTotal >= GOAL_SCORE) &&
(userScore >= 71 || computerScore >= 71 || turnTotal < holdValue))
runOnUiThread(new Runnable() {public void run() {roll();}});
else {
runOnUiThread(new Runnable() {public void run() {hold();}});
break;
}
Thread.yield();
try { Thread.sleep(COMPUTER_DELAY); }
catch (InterruptedException e) { e.printStackTrace(); }
}
}
}).start();
}
private void setButtonsState() {
buttonHold.setEnabled(isUserTurn);
buttonRoll.setEnabled(isUserTurn);
}
private void endGame() {
String message = (!isUserTurn)
? String.format("I win %d to %d.", computerScore, userScore)
: String.format("You win %d to %d.", userScore, computerScore);
message += " Would you like to play again?";
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(message)
.setCancelable(false)
.setPositiveButton("New Game", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
setUserScore(0);
setComputerScore(0);
setTurnTotal(0);
userStartGame = !userStartGame;
isUserTurn = userStartGame;
setButtonsState();
if (isUserTurn)
setImage("roll");
else
computerTurn();
dialog.cancel();
}
})
.setNegativeButton("Quit", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
MainActivity.this.finish();
}
});
AlertDialog alert = builder.create();
alert.show();
}
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("userScore", userScore);
outState.putInt("computerScore", computerScore);
outState.putInt("turnTotal", turnTotal);
outState.putBoolean("userStartGame", userStartGame);
outState.putBoolean("isUserTurn", isUserTurn);
outState.putString("imageName", imageName);
}
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
setUserScore(savedInstanceState.getInt("userScore", 0));
setComputerScore(savedInstanceState.getInt("computerScore", 0));
setTurnTotal(savedInstanceState.getInt("turnTotal", 0));
setImage(savedInstanceState.getString("imageName"));
userStartGame = savedInstanceState.getBoolean("userStartGame", true);
isUserTurn = savedInstanceState.getBoolean("isUserTurn", true);
setButtonsState();
if (userScore >= GOAL_SCORE || computerScore >= GOAL_SCORE)
endGame();
else if (!isUserTurn)
computerTurn();
}
}
Again, there are many possible improvements. This is just a beginning. Here are some ways you can
improve upon and personalize this app:







Experiment with the layout. Larger font sizes, greater button separation, and good use of the
entire screen would be some considerations.
Add sound and/or animation. At this stage, our silent app can be confusing when adjacent roll
results are the same. “Hmm. The image didn’t change. Did my button press register?” This is
especially noticeable when the computer player immediately rolls a 1 after the user. Sounds
and animations can help a user better sense when an action has taken place.
Allow the user to change the computer delay, thus changing the pace of the game.
Collect and display win/loss statistics.
Allow selection of various computer players.
Implement optimal 2-player play, possibly using it to critique and train the user to play Pig
excellently.
Expand the game to multiple players, possibly incorporating networked play.
As one can see, this app provides a good beginning point from which to launch into further learning.
Enjoy!
Next steps:


The Android SDK includes many example apps illustrating commonly used features.
Other Android tutorials/courses are available at
http://code.google.com/edu/android/index.html