Thursday, May 2, 2013

Android Tic Tac Toe Part 3

This is the third and final part of the tutorial series on creating a Tic-Tac-Toe application. See part one about the interfaces and part two about implementing the game logic.

1. Creating a Custom View for the Tiles

In a previous tutorial I described how you could create a custom view in Android that will take attributes and implement custom logic. This is the approach we will take for creating the full Tic Tac Toe application.



This custom view will be called the TicTacToeTileView. This class will extend an image view to take advantage of the ability of an ImageView to easily draw a graphic.

The constructor for the tile will take four boolean attributes, one for each border of the tile. If the boolean is set than that side of the tile, a black border will be drawn.

Listed below is the code for the constructor.

public TicTacToeTile(Context context, AttributeSet attrs) {
 super(context, attrs);

 // Get the border information
 TypedArray a = context.obtainStyledAttributes(attrs,
   R.styleable.com_dreamdom_tictactoe_TicTacToeTile);

 mBorderLeft = a.getBoolean(
   R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderLeft,
   false);
 mBorderRight = a.getBoolean(
   R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderRight,
   false);
 mBorderTop = a.getBoolean(
   R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderTop,
   false);
 mBorderBottom = a.getBoolean(
   R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderBottom,
   false);
 
 // Get the row and column information
 mRow = a.getInteger(R.styleable.com_dreamdom_tictactoe_TicTacToeTile_row, 0);
 mCol = a.getInteger(R.styleable.com_dreamdom_tictactoe_TicTacToeTile_col, 0);
 

 initTile();
}

Since this class implements TicTacToeDrawable, the class is required to implement the setState method. This method will set the tile to one of three possible states - X,O, or blank.

In the TicTacToeTileView's onDraw method is where we will draw the borders. This code is highlighted below.

@Override
protected void onDraw(Canvas canvas) {
 
 // Always call the super
 super.onDraw(canvas);

 // Always draw the borders
 if (mBorderRight == true)
  canvas.drawLine(mWidth, 0, mWidth, mHeight, mBorderPaint);
 if (mBorderLeft == true)
  canvas.drawLine(0, 0, 0, mHeight, mBorderPaint);
 if (mBorderTop == true)
  canvas.drawLine(0, 0, mWidth, 0, mBorderPaint);
 if (mBorderBottom == true)
  canvas.drawLine(0, mHeight, mWidth, mHeight, mBorderPaint);

}


The entire TicTacToeTile.java is listed below

package com.dreamdom.tictactoe;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;

import com.dreamdom.tictactoe.gamedriver.TicTacToeConstants;
import com.dreamdom.tictactoe.gamedriver.TicTacToeDrawable;

public class TicTacToeTile extends ImageView implements TicTacToeDrawable {

 // Constants
 public static final int DEFAULT_PAINT_WIDTH = 10;

 // Dimensions
 private int mWidth;
 private int mHeight;

 // Borders
 private Paint mBorderPaint;
 private boolean mBorderLeft = false;
 private boolean mBorderTop = false;
 private boolean mBorderRight = false;
 private boolean mBorderBottom = false;
 
 // Drawables
 private static Drawable drawableX;
 private static Drawable drawableO;
 private static Drawable drawableBlank;
 
 // Game related
 private int mState;
 private int mRow;
 private int mCol;

 public TicTacToeTile(Context context) {
  super(context);
  initTile();
 }

 public TicTacToeTile(Context context, AttributeSet attrs) {
  super(context, attrs);

  // Get the border information
  TypedArray a = context.obtainStyledAttributes(attrs,
    R.styleable.com_dreamdom_tictactoe_TicTacToeTile);

  mBorderLeft = a.getBoolean(
    R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderLeft,
    false);
  mBorderRight = a.getBoolean(
    R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderRight,
    false);
  mBorderTop = a.getBoolean(
    R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderTop,
    false);
  mBorderBottom = a.getBoolean(
    R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderBottom,
    false);
  
  // Get the row and column information
  mRow = a.getInteger(R.styleable.com_dreamdom_tictactoe_TicTacToeTile_row, 0);
  mCol = a.getInteger(R.styleable.com_dreamdom_tictactoe_TicTacToeTile_col, 0);
  

  initTile();
 }
 
 private void initTile() {
  
  // Create the paint
  mBorderPaint = new Paint();
  mBorderPaint.setStrokeWidth(getContext().getResources()
    .getDisplayMetrics().density
    * DEFAULT_PAINT_WIDTH);
  mBorderPaint.setColor(Color.BLACK);
  
  // Default the state to empty
  mState = TicTacToeConstants.TILE_STATE_EMPTY;
  

 }
 
 public static void setDrawableX(Drawable x) {
  drawableX = x;
 }
 
 public static void setDrawableO(Drawable o) {
  drawableO = o;
 }
 
 public static void setDrawableBlank(Drawable blank) {
  drawableBlank = blank;
 }
 
 public int getRow() {
  return mRow;
 }
 
 public int getCol() {
  return mCol;
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);

  // Save the dimensions
  mWidth = getWidth();
  mHeight = getHeight();
 }

 @Override
 public void setState(int state) {
  mState = state;
  
  if(state == TicTacToeConstants.TILE_STATE_X) {
   setImageDrawable(drawableX);
  } else if (state == TicTacToeConstants.TILE_STATE_O) {
   setImageDrawable(drawableO);
  } else {
   setImageDrawable(drawableBlank);
  }
  
  // force a redraw
  invalidate();
  
 }
 
 @Override
 public int getState() {
  return mState;
 }

 @Override
 protected void onDraw(Canvas canvas) {
  
  // Always call the super
  super.onDraw(canvas);

  // Always draw the borders
  if (mBorderRight == true)
   canvas.drawLine(mWidth, 0, mWidth, mHeight, mBorderPaint);
  if (mBorderLeft == true)
   canvas.drawLine(0, 0, 0, mHeight, mBorderPaint);
  if (mBorderTop == true)
   canvas.drawLine(0, 0, mWidth, 0, mBorderPaint);
  if (mBorderBottom == true)
   canvas.drawLine(0, mHeight, mWidth, mHeight, mBorderPaint);

 }


}

2. Creating a Layout for the Application

Now, we will build a layout for the application. In our layout, we will want to reference the custom view that we created in the first step.

We will also have our layout feature two buttons--one to play the "easy" AI and one to play the "hard" AI. We will also include a checkmark to select whether the user can go first or not, and a label that displays the status of the game.

The full code for the layout main.xml is listed below.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:myapp="http://schemas.android.com/apk/res/com.dreamdom.tictactoe"
 android:orientation="vertical" android:layout_width="fill_parent"
 android:layout_height="fill_parent" android:background="@drawable/bluestripe4">
    <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayoutToolbar" android:layout_gravity="center" android:background="#000000">
        <CheckBox android:layout_width="wrap_content" android:text="@string/player_goes_first" android:layout_height="wrap_content" android:id="@+id/checkBoxPlayerFirst" android:textStyle="bold" android:textColor="#ffffff" android:checked="true"></CheckBox>
        <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/buttonEasyGame" android:text="@string/easy" android:minWidth="75dip"></Button>
        <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/buttonHardGame" android:text="@string/hard" android:minWidth="75dip"></Button>
    </LinearLayout>
    <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayoutMessage" android:background="#000000" android:orientation="vertical">
        <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="3dip" android:layout_gravity="center" android:id="@+id/textViewStatusMessage" android:text="@string/playing_game" android:textSize="18dip"></TextView>
    </LinearLayout>
 <LinearLayout android:layout_width="fill_parent"
  android:id="@+id/linearLayoutGameBoard" android:background="@drawable/whitesquare" android:layout_height="wrap_content" android:padding="10dip" android:layout_margin="20dip">
 
 <TableLayout android:id="@+id/tableLayout1" android:layout_width="fill_parent"
  android:weightSum="3" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip">
  <TableRow android:id="@+id/tableRow1" android:layout_width="wrap_content"
   android:layout_height="wrap_content" android:layout_weight="1">
   <com.dreamdom.tictactoe.TicTacToeTile
    android:adjustViewBounds="true" android:scaleType="fitXY"
    android:id="@+id/GameTile01" android:layout_height="wrap_content" myapp:row="0" myapp:col="0"
    android:layout_weight="1" android:layout_width="fill_parent" android:src="@drawable/blank"/>
   <com.dreamdom.tictactoe.TicTacToeTile
    android:layout_weight="1" android:scaleType="fitXY"
    android:adjustViewBounds="true" android:id="@+id/GameTile02" android:layout_height="wrap_content" 
    myapp:row="0" myapp:col="1"
    myapp:borderLeft="true" myapp:borderRight="true"
    android:layout_width="fill_parent" android:src="@drawable/blank"/>
   <com.dreamdom.tictactoe.TicTacToeTile
    android:adjustViewBounds="true" android:scaleType="fitXY"
    android:layout_weight="1" android:id="@+id/GameTile03"
    myapp:row="0" myapp:col="2"
    android:layout_height="wrap_content" android:layout_width="fill_parent" android:src="@drawable/blank"/>
  </TableRow>
  <TableRow android:id="@+id/tableRow2" android:layout_width="wrap_content"
   android:layout_height="wrap_content" android:layout_weight="1">
   <com.dreamdom.tictactoe.TicTacToeTile
    android:layout_width="wrap_content" android:adjustViewBounds="true"
    android:scaleType="fitXY" android:id="@+id/GameTile04"
    myapp:borderTop="true" myapp:borderBottom="true"
    myapp:row="1" myapp:col="0"
    android:layout_height="wrap_content" android:layout_weight="1" android:src="@drawable/blank"/>
   <com.dreamdom.tictactoe.TicTacToeTile
    android:layout_width="wrap_content" android:layout_height="wrap_content"
    android:layout_weight="1" android:scaleType="fitXY"
    android:adjustViewBounds="true" myapp:borderTop="true" myapp:borderBottom="true"
    myapp:borderLeft="true" myapp:borderRight="true"
    myapp:row="1" myapp:col="1"
    android:id="@+id/GameTile05" android:src="@drawable/blank"/>
   <com.dreamdom.tictactoe.TicTacToeTile
    android:layout_width="wrap_content" android:layout_height="wrap_content"
    android:adjustViewBounds="true" android:scaleType="fitXY"
    android:layout_weight="1" myapp:borderTop="true" myapp:borderBottom="true"
    myapp:row="1" myapp:col="2"
    android:id="@+id/GameTile06" android:src="@drawable/blank"/>
  </TableRow>
  <TableRow android:id="@+id/tableRow3" android:layout_width="wrap_content"
   android:layout_height="wrap_content" android:layout_weight="1">
   <com.dreamdom.tictactoe.TicTacToeTile
    android:layout_width="wrap_content" android:layout_height="wrap_content"
    android:layout_weight="1" android:adjustViewBounds="true"
    myapp:row="2" myapp:col="0"
    android:scaleType="fitXY" android:id="@+id/GameTile07" android:src="@drawable/blank"/>
   <com.dreamdom.tictactoe.TicTacToeTile
    android:layout_width="wrap_content" android:layout_height="wrap_content"
    android:layout_weight="1" android:scaleType="fitXY"
    android:adjustViewBounds="true" myapp:borderLeft="true" myapp:borderRight="true"
    myapp:row="2" myapp:col="1"
    android:id="@+id/GameTile08" android:src="@drawable/blank"/>
   <com.dreamdom.tictactoe.TicTacToeTile
    android:layout_width="wrap_content" android:layout_height="wrap_content"
    android:adjustViewBounds="true" android:scaleType="fitXY"
    android:layout_weight="1" myapp:row="2" myapp:col="2"
    android:id="@+id/GameTile09" android:src="@drawable/blank"/>
  </TableRow>
 </TableLayout>
  
 </LinearLayout>
</LinearLayout&gt


3. Creating the GameActivity Class

Lastly, we must create the GameActivity class. This class will be fairly straightforward. Since most of the logic is contained in the GameDriver package, the GameActivity class will mostly be responsible for listening for button clicks, and passing on the value of these button clicks to the game manager.

The code for the GameActivity class is listed below.

package com.dreamdom.tictactoe;

import java.util.ArrayList;

import com.dreamdom.tictactoe.gamedriver.ClassicGameManager;
import com.dreamdom.tictactoe.gamedriver.EasyGameAI;
import com.dreamdom.tictactoe.gamedriver.GameManager;
import com.dreamdom.tictactoe.gamedriver.HardGameAI;
import com.dreamdom.tictactoe.gamedriver.TicTacToeConstants;
import com.dreamdom.tictactoe.gamedriver.TicTacToeDrawable;

import android.app.Activity;
import android.content.res.Resources;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;
import android.widget.CompoundButton.OnCheckedChangeListener;

/**
 * GameActivity is used to handle 3 by 3 tic tac toe games
 * @author Dominic
 *
 */
public class GameActivity extends Activity {
 
 public static final int[] GAME_TILE_IDS = { R.id.GameTile01,
   R.id.GameTile02, R.id.GameTile03, R.id.GameTile04, R.id.GameTile05,
   R.id.GameTile06, R.id.GameTile07, R.id.GameTile08, R.id.GameTile09 };

 private GameManager mGameManager;
 private TileClickHandler mClickHandler;
 private EasyGameAI mEasyGameAI;
 private HardGameAI mHardGameAI;
 private TextView mTextViewStatus;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // Set the main content view
        setContentView(R.layout.main);
        
        // Set the drawables for the tiles
        TicTacToeTile.setDrawableX(getResources().getDrawable(R.drawable.x));
        TicTacToeTile.setDrawableO(getResources().getDrawable(R.drawable.circle));
        TicTacToeTile.setDrawableBlank(getResources().getDrawable(R.drawable.blank));
        
        ArrayList<TicTacToeDrawable> tileList = new ArrayList<TicTacToeDrawable>(GAME_TILE_IDS.length);
        mClickHandler = new TileClickHandler();
        
        // Set the click handler for all the tiles and build up a list
        for(int i=0; i<GAME_TILE_IDS.length; i++) {
         TicTacToeTile curTile = (TicTacToeTile) findViewById(GAME_TILE_IDS[i]);
         curTile.setOnClickListener(mClickHandler);
         tileList.add(curTile);
        }
        
        mEasyGameAI = new EasyGameAI();
        mHardGameAI = new HardGameAI();
        
        // Default to using the EasyGameAI
        mGameManager = new ClassicGameManager(tileList, mEasyGameAI);
        CheckBox playerGoesFirst = (CheckBox) findViewById(R.id.checkBoxPlayerFirst);
        if(playerGoesFirst.isChecked())
         mGameManager.setStartingTurn(TicTacToeConstants.TURN_PLAYER);
        else
         mGameManager.setStartingTurn(TicTacToeConstants.TURN_COMPUTER);
        
        
        // Set whether the player should go first or not
        playerGoesFirst.setOnCheckedChangeListener(new OnCheckedChangeListener() {
   
   @Override
   public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    if(isChecked)
     mGameManager.setStartingTurn(TicTacToeConstants.TURN_PLAYER);
    else
     mGameManager.setStartingTurn(TicTacToeConstants.TURN_COMPUTER);
   }
  });
        
        // Start new easy game
        Button easyButton = (Button) findViewById(R.id.buttonEasyGame);
        easyButton.setOnClickListener(new OnClickListener() {
   
   @Override
   public void onClick(View v) {
    // Set the AI to the easy AI
    mGameManager.setGameAI(mEasyGameAI);
    mGameManager.reset();
    updateStatusMessage();
   }
  });
        
        // Start new hard game
        Button hardButton = (Button) findViewById(R.id.buttonHardGame);
        hardButton.setOnClickListener(new OnClickListener() {
   
   @Override
   public void onClick(View v) {
    // Set the AI to the hard AI
    mGameManager.setGameAI(mHardGameAI);
    mGameManager.reset();
    updateStatusMessage();
   }
  });
        
        // Save the text view for the status
        mTextViewStatus = (TextView) findViewById(R.id.textViewStatusMessage);
        
    }
    
    /**
     * Will set the status message based on the current game state
     */
    public void updateStatusMessage() {
     
     // Get the games current state
     int gameState = mGameManager.getGameState();
     Resources res = getResources();
     
     if(gameState == TicTacToeConstants.GAME_STATE_PLAYING) {
      mTextViewStatus.setText(res.getString(R.string.playing_game));
     } else if(gameState == TicTacToeConstants.GAME_STATE_PLAYER_WINS) {
      mTextViewStatus.setText(res.getString(R.string.player_wins));
     } else if (gameState == TicTacToeConstants.GAME_STATE_COMPUTER_WINS) {
      mTextViewStatus.setText(res.getString(R.string.computer_wins));
     } else if (gameState == TicTacToeConstants.GAME_STATE_CATS_GAME) {
      mTextViewStatus.setText(res.getString(R.string.tie_game));
     }
    }
    
    /**
     * Private class to handle when a tile is clicked
     * @author Dominic
     *
     */
    private class TileClickHandler implements OnClickListener {

  @Override
  public void onClick(View v) {
   if (mGameManager.getGameState() == TicTacToeConstants.GAME_STATE_PLAYING
     && v instanceof TicTacToeDrawable) {

    // Click the tile
    TicTacToeDrawable tile = (TicTacToeDrawable) v;
    if(tile.getState() == TicTacToeConstants.TILE_STATE_EMPTY) {
     mGameManager.playerClickedTile(tile);
    }
    
    // Update the status message
    updateStatusMessage();
   }
  }
     
    }
}


4. That's It

Screenshot of the completed game


Thanks for taking the time to read through the Tic Tac Toe tutorial series. This is a simple version of the game that we created, but hopefully the tutorial was a great learning experience!

4 comments: