5COSC005W - Tutorial 11 Exercises

As part of this tutorial for this week, you will revise the knowlegde you obtained for the whole module by creating some new apps. You should complete ALL the tasks described in the following links and specifications:

1 A Tic-Tac-Toe Game

a) Create a new project and implement the following code (the code is adopted from your Reading List from the book Hello Android, by Ed Burnette). The program allows the user to play the tic-tac-toe (noughts and crosses) game against the computer.

The game is played with two players, X (user) and O (computer), who take turns marking the spaces in a 3×3 grid. The X player goes first. The player who succeeds in placing three respective marks in a horizontal, vertical, or diagonal row wins the game (see Figure below).

tic_tac_toe.png

Figure 1: An instance of the tic-tac-toe board where the X player has won since he managed to place three 'X' in a diagonal.

In your implementation the first screen that the user sees is the game board (there are no buttons or menus to start the game). The application should detect by itself when one of the two players wins and terminate the game with a message indicating the winner or that the result is a draw. For simplicity, the game can be played only once, i.e. there is no ``new game'' button which draws a new board.

The user X, starts first, while the computer (O) responds with a random move, i.e. it places an 'O' in a random cell which is not already occupied by another 'O' or an 'X'.

Below, you will find the full code for the application. Study the code and make sure that you understand it. Ask your tutor if something is unclear.

The main activity is the following. Notice that the layout is created programmatically using a custom view which is described next.

package uk.ac.westminster.tictactoe;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import java.util.Random;


public class TicTacToe extends AppCompatActivity {
    char puzzle[][] = new char[3][3];
    Random generator = new Random();

    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new BoardView(this));
    }


    boolean isTileValid(int x, int y) {
        if (puzzle[x][y] != 'X' && puzzle[x][y] != 'O') {
            return true;
        } else
            return false;
    }


    void setTile(int x, int y, char value) {
        puzzle[x][y] = value;
    }


    String getTile(int x, int y) {
        return "" + puzzle[x][y];
    }


    boolean isBoardFull() {
        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++) {
                if (puzzle[i][j] != 'X' && puzzle[i][j] != 'O')
                    return false;
            }

        return true;
    }


    /* Check if 'O' or 'X' has won and return the corresponding character
     * or blank if still on play */
    char checkForWin() {
        for (int i = 0; i < 3; i++) {
            // check columns
            char c = puzzle[i][0];
            if (puzzle[i][1] == c && puzzle[i][2] == c)
                return c;
            // check rows
            c = puzzle[0][i];
            if (puzzle[1][i] == c && puzzle[2][i] == c)
                return c;
        }

        // check diagonals
        char c = puzzle[0][0];
        if (puzzle[1][1] == c && puzzle[2][2] == c)
            return c;
        c = puzzle[0][2];
        if (puzzle[1][1] == c && puzzle[2][0] == c)
            return c;

        return ' ';
    }


    // calculate machine (O) response to X player move
    void machineResponse() {
        if (isBoardFull())
            return;
        // find a random valid position
        int x = -1;
        int y = -1;

        while ((x == -1 && y == -1) || !isTileValid(x, y)) {
            x = generator.nextInt(3);
            y = generator.nextInt(3);
        }

        setTile(x, y, 'O');
    }
}

The custom view should be created in a separate class (and file) as the following:

package uk.ac.westminster.tictactoe;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.view.KeyEvent;
import android.view.View;

public class BoardView extends View {
    private TicTacToe game;
    private float width; // width of one tile
    private float height; // height of one tile
    private float left_offset; // offset from left where grid starts
    private float top_offset; // offset from top where grid starts

    private int selX; // X index of selection
    private int selY; // Y index of selection
    private final Rect selRect = new Rect();
    Paint foreground = new Paint(Paint.ANTI_ALIAS_FLAG);
    boolean gameOver = false;
    char winner = ' ';  // the winner of the game

    public BoardView(Context context) {
        super(context);
        this.game = (TicTacToe) context;
        setFocusable(true);
        setFocusableInTouchMode(true);
    }


    private void getRect(int x, int y, Rect rect) {
        rect.set((int) (x * width + left_offset),
                (int) (y * height + top_offset),
                (int) (x * width + width + left_offset),
                (int) (y * height + height + top_offset));
    }


    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        width = w / 9f;
        height = h / 9f;
        left_offset = 3 * width;
        top_offset = 3 * height;
        getRect(selX, selY, selRect);
        super.onSizeChanged(w, h, oldw, oldh);
    }


    protected void onDraw(Canvas canvas) {
        // Draw the background...
        Paint background = new Paint();
        background.setColor(getResources().getColor(
                R.color.puzzle_background));
        canvas.drawRect(0, 0, getWidth(), getHeight(),
                background);

        // Define colors for the grid lines
        Paint dark = new Paint();
        dark.setColor(getResources().getColor(R.color.puzzle_dark));

        // draw the grid
        for (int i = 0; i < 4; i++) {
            canvas.drawLine(left_offset, i * height + top_offset,
                    getWidth() - left_offset, i * height + top_offset, dark);
            canvas.drawLine(i * width + left_offset, top_offset,
                    i * width + left_offset, getHeight() - top_offset, dark);
        }

        // draw the selection
        Paint selected = new Paint();
        selected.setColor(getResources().getColor(
                R.color.puzzle_selected));
        canvas.drawRect(selRect, selected);

        // draw filled positions
        foreground.setColor(getResources().getColor(
                R.color.puzzle_foreground));
        foreground.setStyle(Style.FILL);
        foreground.setTextSize(height * 0.75f);
        foreground.setTextScaleX(width / height);
        foreground.setTextAlign(Paint.Align.CENTER);
        // Draw the number in the center of the tile
        FontMetrics fm = foreground.getFontMetrics();
        // Centering in X: use alignment (and X at midpoint)
        float x = width / 2;
        // Centering in Y: measure ascent/descent first
        float y = height / 2 - (fm.ascent + fm.descent) / 2;

        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++) {
                canvas.drawText(game.getTile(i, j),
                        i * width + x + left_offset,
                        j * height + y + top_offset, foreground);
            }

        //char winner = game.checkForWin();
        if (winner == 'X') {
            canvas.drawText("You Won!!!", 2 * width + left_offset, 2 * height + 2 * top_offset, foreground);
            gameOver = true;
        } else if (winner == 'O') {
            canvas.drawText("I Won!!!", 2 * width + left_offset,
                    2 * height + 2 * top_offset, foreground);
            gameOver = true;
        }
    }

    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (gameOver)
            return false;

        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_UP:
                select(selX, selY - 1);
                break;
            case KeyEvent.KEYCODE_DPAD_DOWN:
                select(selX, selY + 1);
                break;
            case KeyEvent.KEYCODE_DPAD_LEFT:
                select(selX - 1, selY);
                break;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                select(selX + 1, selY);
                break;
            case KeyEvent.KEYCODE_ENTER:
            case KeyEvent.KEYCODE_DPAD_CENTER:
                setSelectedTile();
                break;
            default:
                return super.onKeyDown(keyCode, event);
        }
        return true;
    }

    private void select(int x, int y) {
        invalidate(selRect);
        selX = Math.min(Math.max(x, 0), 2);
        selY = Math.min(Math.max(y, 0), 2);
        getRect(selX, selY, selRect);
        invalidate(selRect);
    }

    public void setSelectedTile() {
        if (game.isTileValid(selX, selY)) {
            game.setTile(selX, selY, 'X');
            winner = game.checkForWin();
            if (winner != 'X') {
                game.machineResponse();
                winner = game.checkForWin();
            }
            invalidate();
        }
    }
}

The strings.xml file:

<resources>
    <string name="app_name">TicTacToe</string>
    <string name="hello">Hello World, TicTacToe!</string>
</resources>

The colors.xml file:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#6200EE</color>
    <color name="colorPrimaryDark">#3700B3</color>
    <color name="colorAccent">#03DAC5</color>
    <color name="puzzle_background">#ffe6f0ff</color>
    <color name="puzzle_dark">#6456648f</color>
    <color name="puzzle_foreground">#ff000000</color>
    <color name="puzzle_selected">#64ff8000</color>
</resources>

You can play the game by moving to the desired cell and pressing the ENTER button. Make sure that you understand fully the code.

b) Modify the code so that the computer plays in an intelligent way, i.e. first it checks if it can win by placing an 'O' in a cell, if not it checks if it can obstruct a human win by placing an 'O' in a cell, otherwise it places an 'O' in a cell so as to create an opportunity to win in the next move (i.e. it places an 'O' in a cell so as to form two 'O's in a row, column or diagonal.

c) Modify the application so that it displays on the top of the activity how many wins in total the human has and how many the computer has since the beginning of first using the application.

Hint: Use SharedPreferences to save the number of wins for the two opponents.