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