Android Pig Development Tutorial
Todd Neller, Gettysburg College, October 12th, 2016
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 Eclipse with the Android SDK already installed( 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: AVD (can name this anything intuitive to you)
- Target: API Level 22(or 23)
- Click “Create AVD”.
- Close Android AVD Manager window.
Or you can Clone a device from the Device Definitions tab. The important thing is to select one that is API level 22 or 23. Next, we create a new Android project that starts with a sort of “Hello, world!” do-nothing app skeleton.
- File New Android Application Project
- Call application name “Pig”, the project “Android Pig”, and the package name “edu.gettysburg.pig”. Select the Minimum Required SDK to be “API 22: Android 5.1.1”, and the Target SDK to be “API 23: Android 6.0”
- Click the “Next>” button 4 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. (You’ll need to create this. All other drawable folders are for specific DPI ranges.)
- In res layout main.xml is the XML (eXtensibleMarkup 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.
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>-F11[3] 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,
- Create a res/drawable subdirectory in your project.
- Copy the provided Pig app image files[4] 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 / Name / ValueString / your_score / Your Score:
String / my_score / My Score:
String / turn_total / Turn Total:
String / roll / Roll
String / hold / Hold
String / zero / 0
Color / black / #000000
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
stringname="app_name"Android Pig 2012</string
stringname="hello_world"Hello world!/string
stringname="menu_settings"Settings</string
stringname="title_activity_main"Pig</string
stringname="your_score"Your Score:</string
stringname="my_score"My Score:</string
stringname="turn_total"Turn Total:</string
stringname="roll"Roll</string
stringname="hold"Hold</string
stringname="zero"0</string
colorname="black"#000000</color
colorname="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 TextViewelements 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:
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 right-click, chooseOther 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:
LinearLayoutxmlns:android="
xmlns: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:
- Define variables and bind them to resources and GUI elements.
- Set up means to update our TextView labels and ImageView image, testing it with a simple behavior: die rolling with appropriate turn total updates.
- Add a hold action that adds the turn total to the current player’s score, and resets the turn total to zero
- Add a turn changing behavior that changes the current player.
- Introduce a computer player, disabling buttons when the computer is playing, and showing how one can interact with the GUI thread from another thread.
- Detect when a player wins, report the win, and ask the user whether to play again or not.
- 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
protectedstaticfinallongCOMPUTER_DELAY = 1000;
// GOAL_SCORE - goal score at or above which the holding player wins
privatestaticfinalintGOAL_SCORE = 100;
// Game state variables:
privateintuserScore = 0, computerScore = 0, turnTotal = 0;
// userStartGame - whether or not the user starts the current game
privatebooleanuserStartGame = true;
// isUserTurn - whether or not it is currently the user's turn
privatebooleanisUserTurn = true;
// imageName - name of the current displayed image
private String imageName = "roll";
// GUI views
privateTextViewtextViewYourScore, textViewMyScore, textViewTurnTotal;
privateImageViewimageView;
// GUI buttons
private Button buttonRoll, buttonHold;
// mapping from image strings to Drawable resources
privateHashMap<String, DrawabledrawableMap = newHashMap<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", getDrawable(R.drawable.roll));
drawableMap.put("hold", getDrawable(R.drawable.hold));
drawableMap.put("die1", getDrawable(R.drawable.die1));
drawableMap.put("die2", getDrawable(R.drawable.die2));
drawableMap.put("die3", getDrawable(R.drawable.die3));
drawableMap.put("die4", getDrawable(R.drawable.die4));
drawableMap.put("die5", getDrawable(R.drawable.die5));
drawableMap.put("die6", 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 getDrawable 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:
privatevoidsetUserScore(finalintnewScore) {
userScore = newScore;
textViewYourScore.setText(String.valueOf(newScore));
}
privatevoidsetComputerScore(finalintnewScore) {
computerScore = newScore;
textViewMyScore.setText(String.valueOf(newScore));
}
privatevoidsetTurnTotal(finalintnewTotal) {
turnTotal = newTotal;
textViewTurnTotal.setText(String.valueOf(newTotal));
}
privatevoidsetImage(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(newView.OnClickListener() {
publicvoidonClick(View v) {
roll();
}
});
buttonHold.setOnClickListener(newView.OnClickListener() {
publicvoidonClick(View v) {
hold();
}
});
Accordingly, create private roll and hold methods:
privatevoid roll() {
}
privatevoid 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:
privatevoid 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:
privatevoid roll() {
int roll = random.nextInt(6) + 1;