Upload
others
View
15
Download
0
Embed Size (px)
Citation preview
Android Pig Development Tutorial
Todd Neller, Gettysburg College, November 15th, 2017
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 Android Studio with the
Android SDK already installed (http://developer.android.com/sdk/installing.html).
1 http://cs.gettysburg.edu/~tneller/resources/pig/cs1/gui.html
2 http://cs.gettysburg.edu/projects/pig/
Opening Android Studio, we “Start a new Android Studio project”. In the “Create Android Project”
window, set your application name to “Pig”, and form your company domain by joining your username
and your internet domain with a period, e.g. “tneller.gettysburg.edu”. This will make a unique package
name from the reversed domain and your application name, e.g. “edu.gettysburg.tneller.pig”. Your
project location can be left with its default value. Press the “Next” button.
In the “Target Android Devices” window, keep the defaults and press the “Next” button. In the “Add an
Activity to Mobile” window, keep the defaults (add an Empty Activity) and press the “Next” button. In
the “Configure Activity” window, keep the defaults and press the “Next” button. If the “Component
Installer” window appears, press the “Finish” button.
If you encounter “Gradle sync” errors, follow the “____ and sync” error resolution suggestions in the
bottom window. You should get to a blank main activity starting point that looks like this:
First, we’ll test that we can emulate this blank activity by clicking the green play “Run App” button (or
Shift+F10). If you have not done so already, you will need to “Create New Virtual Device”. Leave the
default selection and press “Next”. In the “System Image” window, you will need to select an image/API
level for your app. If you select an older image (i.e. lower API level), more current devices will be able to
run the app. If you select a newer image (i.e. higher API level), you will have the advantage of all of the
latest improvements to the Android API. For our project, we will select Android release 8.0 (i.e. “Oreo”,
API level 26). Press “Next” and then “Finish” on the next “Android Virtual Device (AVD)” screen.
Back in the Select Deployment Target window where you first clicked “Create New Virtual Device”, you
should now be able to select the device you created and press “OK”. The Android Emulator will start,
and after a long startup delay, you should see your blank activity start with the “Pig” title in the upper-
left, and the message “Hello World!”. Leave the emulator running and change back to your Android
Studio window.
In the left “Project” pane, click the triangle next to the “app” folder and note these 3 subfolders:
manifests – This contains your top-level project configuration file called “AndroidManifest.xml”,
where one’s definitions for app name, theme, initial activity, launcher icon, etc. may be found.
java – This contains your Java class definitions for app activities, etc. Your app’s behavior is
defined here.
res (resource files) – This contains resources for your app:
o drawable – This contains app images.
o layout – This contains XML (eXtensible Markup Language) specifications of activity GUI
layouts.
o mipmap – This contains app and launcher icons only. All other icons go in the drawable
folder.
o values – This contains XML specifications of colors, strings, and styles used in your app.
We need to first add our project images to the project:
Copy the provided Pig app image files3 into the res/drawable subdirectory of your project. To
find this folder, first navigate to where you initially created your “AndroidStudioProjects” folder
(usually the user directory). Within this folder, you’ll find the drawable directory via path
“AndroidStudioProjects/Pig/app/src/main/res/drawable”.
In the Project pane, open the app res drawable folders to verify that the image files are
present.
Next, we need to define the colors “black” and “white” for our project:
Copy one of the “color” lines of the XML and paste two additional copies to modify as follows:
<color name="black">#000000</color>
<color name="white">#FFFFFF</color>
Next, we will define the strings that we’ll use in our app. This may seem unnecessarily complicated, but
the benefit to the app developer is that the creation of app translations to other languages becomes
much easier when all that is entailed is creating a parallel strings.xml file for each supported language.
For example, from https://developer.android.com/training/basics/supporting-devices/languages.html
we see the relative ease of additional language support:
To add support for more locales, create additional directories inside res/. Each directory's name should adhere to the following format:
<resource type>-b+<language code>[+<country code>]
For example, values-b+es/ contains string resources for locales with the language code es. Similarly, mipmap-b+es+ES/ contains icons for locales with the es language code and the ES country code.
For now, we’ll provide only default support for English. Open the res values strings.xml file. Copy
the string XML line and paste six copies, and modify them to have these additional string value
definitions:
3 http://cs.gettysburg.edu/~tneller/resources/pig/cs1/images/png/
<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>
Once you have saved them (Ctrl-S), you can now close your .xml file tabbed panes. From the
MainActivity.java tabbed pane, you can get to the activity_main.xml by either (1) clicking its tab, or
clicking the icon to the left of the beginning of the class definition that takes you to the “Related XML
file”. Here, we will build our main activity GUI.
We’d like to create a layout that looks like this:
In the Component Tree pane, select and delete the TextView component (“Hello World!”).
In the Palette pane, select Layouts on the left and drag LinearLayout (Vertical) into the
ConstraintLayout in the Component Tree pane.
Next, we’ll drag three elements from the Graphical Layout pane into the LinearLayout element of the
Component Tree pane:
Layouts TableLayout
Images ImageView (Select the “roll” image when prompted.)
Layouts TableLayout
Place each at the bottom of previous elements.
Expand the LinearLayout to see these three components by clicking the triangle next to LinearLayout.
Next select the first TableLayout element from the Component Tree pane. If there are not already 3 or
more TableRows in the table layout, drag three Layout TableRow components. If there are more,
delete any extra table rows.
The second TableLayout component should be modified to have only one TableRow. At this point, your
Component Tree pane should look something like this:
Next, we’ll add two Text TextView components to each of the first three table rows. Also, we’ll add
two Form Widgets Button components to the single row of the bottom TableLayout. At this point,
your Component Tree pane should look something like this:
For now, don’t worry if the names don’t match those shown here.
Next, it would be nice to center these components and expand table row components to fill the width of
the screen. Now that we have the components in place, we’ll see what changing some of their
Properties can do. By clicking on a component in the Component Tree or in the graphical rendering to
the right, a component’s properties are shown in the right “Attributes” pane. For example, in the
component tree, click the LinearLayout, and click “View all attributes” in the Attributes pane. Under
Theme, click “…” to the right of “background” and select Color black and click “OK”. In the graphical
rendering of the layout, you should be able to see that the background color is now black.
It is also possible to change attributes of a number of components at once. In the Component Tree
pane, click the first TextView component and then control-click each of the remaining five TextView
components. In the Attributes pane, scroll to attribute “textColor”, click the … to the right, and select
Color white and click “OK”.
Next, select all six TextView and two Button components in the Component Tree window, click “View
fewer attributes” (or the double arrow icon up top), and enter the number 1 under “layout_weight”.
This will extend the width of the TextView and Button components.
Center the image by selecting the ImageView component, and (under “all attributes”) open the options
for “layout_gravity” and select “center”. Now the image will be centered.
Select all three left column TextView components and set their “gravity” attribute to “right”. Now the
text will be right-justified.
Select all TextView components and set their “paddingHorizontal” to “4pt”. Now there is a horizontal
padding to separate the left and right column text. At this point, your graphical rendering of the layout
should look something like this:
If you do not see the roll image in the image view, you likely need to drag it upward from the bottom
center of your graphical layout.
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:
textView: Select attribute “text”, select string “your_score”, and click “OK”.
textView3: Select attribute “text”, select string “my_score”, and click “OK”.
textView5: Select attribute “text”, select string “turn_total”, and click “OK”.
Select textView2, textView4, and textView6: Select attribute “text”, select string “zero”, and
click OK.
textView2: Select attribute “ID” and set to “textViewYourScore”.
textView4: Select attribute “ID” and set to “textViewMyScore”.
textView6: Select attribute “ID” and set to “textViewTurnTotal”.
imageView: Select attribute “ID” and set to “imageView” (if it’s not set to “imageView” already).
button: Select attribute “ID” and set to “buttonRoll”. Select attribute “text”, select string “roll”,
and click “OK”.
button2: Select attribute “ID” and set to “buttonHold”. Select attribute “text”, select string
“hold”, and click “OK”.
Finally, select all components in the Component tree pane (e.g. with Control-A), and set attribute
“layout_width” to “match_parent”.
Now, your Graphical Layout and Outline should look something like this:
And the main_activity.xml (under the bottom “Text” tab) should look something like this:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="edu.gettysburg.tneller.pig.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="right"
android:paddingHorizontal="4pt"
android:text="@string/your_score"
android:textColor="@color/white" />
<TextView
android:id="@+id/textViewYourScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/zero"
android:textColor="@color/white" />
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="right"
android:paddingHorizontal="4pt"
android:text="@string/my_score"
android:textColor="@color/white" />
<TextView
android:id="@+id/textViewMyScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/zero"
android:textColor="@color/white" />
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="right"
android:paddingHorizontal="4pt"
android:text="@string/turn_total"
android:textColor="@color/white" />
<TextView
android:id="@+id/textViewTurnTotal"
android:layout_width="match_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="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:scaleType="center"
app:srcCompat="@drawable/roll" />
<TableLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/buttonRoll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/roll" />
<Button
android:id="@+id/buttonHold"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/hold" />
</TableRow>
</TableLayout>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
One can make many improvements to this layout, but this suffices for our demonstration purposes.
Perform 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.
In what follows, I assume that you will add necessary class imports as we make use of them. In Android
Studio, Alt-Enter when the cursor is on an unimported class name will automatically add an
unambiguous import.
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";
// computerThread - thread for computer player
private Thread computerThread = null;
// 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<>();
// 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 = findViewById(R.id.textViewYourScore);
textViewMyScore = findViewById(R.id.textViewMyScore);
textViewTurnTotal = findViewById(R.id.textViewTurnTotal);
buttonRoll = findViewById(R.id.buttonRoll);
buttonHold = findViewById(R.id.buttonHold);
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 Drawable image resources, we use:
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” strategy4. 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.
We start a computer player thread with the following method:
private void startComputerThread() {
if (computerThread == null) {
computerThread = new Thread(new Runnable() {
public void run() {
while (true) {
Thread.yield();
try {Thread.sleep(COMPUTER_DELAY);} catch (InterruptedException e) {break;}
if (!isUserTurn && userScore < GOAL_SCORE && computerScore < GOAL_SCORE) {
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();}});
4 Practical Play of the Dice Game Pig, The UMAP Journal 31(1) (2010), pp. 5-19.
else {
runOnUiThread(new Runnable() {public void run() {hold();}});
}
}
}
}
});
computerThread.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.
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();
}
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(Locale.getDefault(), "I win %d to %d.", computerScore, userScore)
: String.format(Locale.getDefault(), "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");
dialog.cancel();
}
})
.setNegativeButton("Quit", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
computerThread.interrupt();
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);
computerThread.interrupt();
computerThread = null;
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();
startComputerThread();
}
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? Also, we need to restart the computer
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.tneller.pig;
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.HashMap;
import java.util.Locale;
import java.util.Random;
public class MainActivity extends AppCompatActivity {
// 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";
// computerThread - thread for computer player
private Thread computerThread = null;
// 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<>();
// 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.activity_main);
textViewYourScore = findViewById(R.id.textViewYourScore);
textViewMyScore = findViewById(R.id.textViewMyScore);
textViewTurnTotal = findViewById(R.id.textViewTurnTotal);
buttonRoll = findViewById(R.id.buttonRoll);
buttonHold = findViewById(R.id.buttonHold);
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();
}
});
startComputerThread();
}
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();
}
private void startComputerThread() {
if (computerThread == null) {
computerThread = new Thread(new Runnable() {
public void run() {
while (true) {
Thread.yield();
try {Thread.sleep(COMPUTER_DELAY);}
catch (InterruptedException e) {break;}
if (!isUserTurn && userScore < GOAL_SCORE && computerScore < GOAL_SCORE) {
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();}});
}
}
}
}
});
computerThread.start();
}
}
private void setButtonsState() {
buttonHold.setEnabled(isUserTurn);
buttonRoll.setEnabled(isUserTurn);
}
private void endGame() {
String message = (!isUserTurn)
? String.format(Locale.getDefault(), "I win %d to %d.", computerScore, userScore)
: String.format(Locale.getDefault(), "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");
dialog.cancel();
}
})
.setNegativeButton("Quit", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
computerThread.interrupt();
MainActivity.this.finish();
}
});
AlertDialog alert = builder.create();
alert.show();
}
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
computerThread.interrupt();
computerThread = null;
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();
startComputerThread();
}
}
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.
Android tutorials/courses are available at https://developer.android.com/training/index.html