19
Android Pig Development Tutorial Todd Neller, Gettysburg College, November 15 th , 2017 Before we begin developing, we first introduce the game we’ll be developing. 1 The game of Pig 2 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/

Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

  • Upload
    others

  • View
    15

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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/

Page 2: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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.

Page 3: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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/

Page 4: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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

Page 5: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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.

Page 6: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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

Page 7: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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

Page 8: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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

Page 9: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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

Page 10: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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

Page 11: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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();

Page 12: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

}

});

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

Page 13: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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.

Page 14: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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();

Page 15: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

}

});

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:

Page 16: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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;

Page 17: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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

}

Page 18: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

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();

}

})

Page 19: Android Pig Development Tutorial - Gettysburg Collegecs.gettysburg.edu/~tneller/resources/android/Android Pig... · 2017-11-15 · Android Pig Development Tutorial Todd Neller, Gettysburg

.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