These are notes on making a TicTacToe game on Android.
Based on:
Create Android App with Empty Activity
Create the GameEngine class. The GameEngine class will store the board as elements of 3x3 grid in an array and will contain a boolean indicating if the game is ended.
The play method will let you set the mark of the currentPlayer
on the board at a given (x,y) position.
package com.learning.tictactoegame;
import java.util.Random;
public class GameEngine {
private static final Random RANDOM = new Random();
private char [] elements;
private char currentPlayer;
private boolean ended;
public GameEngine() {
elements = new char[9];
newGame();
}
public boolean isEnded() {
return ended;
}
public char play(int x, int y) {
if(!ended && elements[3*y+x] == ' ') {
elements[3*y+x] = currentPlayer;
changePlayer();
}
return checkEnd();
}
public void changePlayer() {
currentPlayer = (currentPlayer == 'X' ? 'O' : 'X');
}
public char getElement(int x, int y) {
return elements[3*y+x];
}
public void newGame() {
for(int i = 0; i < elements.length; i++) {
elements[i] = ' ';
}
currentPlayer = 'X';
ended = false;
}
public char checkEnd() {
for (int i = 0; i < 3; i++) {
// columns
if (getElement(i, 0) != ' ' &&
getElement(i, 0) == getElement(i, 1) &&
getElement(i, 1) == getElement(i, 2)) {
ended = true;
return getElement(i, 0);
}
// rows
if (getElement(0, i) != ' ' &&
getElement(0, i) == getElement(1, i) &&
getElement(1, i) == getElement(2, i)) {
ended = true;
return getElement(0, i);
}
}
// backslash
if (getElement(0, 0) != ' ' &&
getElement(0, 0) == getElement(1, 1) &&
getElement(1, 1) == getElement(2, 2)) {
ended = true;
return getElement(0, 0);
}
// slash
if (getElement(2, 0) != ' ' &&
getElement(2, 0) == getElement(1, 1) &&
getElement(1, 1) == getElement(0, 2)) {
ended = true;
return getElement(2, 0);
}
// tie
for (int i = 0; i < 9; i++) {
if (elements[i] == ' ') {
return ' ';
}
}
return 'T';
}
public char computer() {
if (!ended) {
int position = -1;
do {
position = RANDOM.nextInt(9);
} while (elements[position] != ' ');
elements[position] = currentPlayer;
changePlayer();
}
return checkEnd();
}
}
Create BoardView.class
based off of View
.
Create drawGrid()
and drawBoard()
and paint game elements. Use onTouchEvent
with MotionEvent.ACTION_DOWN
to determine user input.
package com.learning.tictactoegame;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class BoardView extends View {
private static final int LINE_THICK = 5;
private static final int ELEMENT_MARGIN = 20;
private static final int ELEMENT_STROKE_WIDTH = 15;
private int width, height, elementWidth, elementHeight;
private Paint gridPaint, oPaint, xPaint;
private GameEngine gameEngine;
private MainActivity activity;
public BoardView(Context context) {
super(context);
}
public BoardView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
gridPaint = new Paint();
oPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
oPaint.setColor(Color.RED);
oPaint.setStyle(Paint.Style.STROKE);
oPaint.setStrokeWidth(ELEMENT_STROKE_WIDTH);
xPaint = new Paint(oPaint);
xPaint.setColor(Color.BLUE);
}
public void setMainActivity(MainActivity a) {
activity = a;
}
public void setGameEngine(GameEngine g) {
gameEngine = g;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
width = View.MeasureSpec.getSize(widthMeasureSpec);
height = View.MeasureSpec.getSize(heightMeasureSpec);
elementWidth = (width - LINE_THICK) / 3;
elementHeight = (height - LINE_THICK) / 3;
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
drawGrid(canvas);
drawBoard(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(!gameEngine.isEnded() && event.getAction() == MotionEvent.ACTION_DOWN) {
int x = (int) (event.getX() / elementWidth);
int y = (int) (event.getY() / elementHeight);
char win = gameEngine.play(x, y);
invalidate();
if (win != ' ') {
activity.gameEnded(win);
} else {
// computer plays...
win = gameEngine.computer();
invalidate();
if (win != ' ') {
activity.gameEnded(win);
}
}
}
return super.onTouchEvent(event);
}
private void drawBoard(Canvas canvas) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
drawElement(canvas, gameEngine.getElement(i, j), i, j);
}
}
}
private void drawGrid(Canvas canvas) {
for (int i = 0; i < 2; i++) {
// vertical lines
float left = elementWidth * (i + 1);
float right = left + LINE_THICK;
float top = 0;
float bottom = height;
canvas.drawRect(left, top, right, bottom, gridPaint);
//horizintal lines
float left2 = 0;
float right2 = width;
float top2 = elementHeight * (i + 1);
float bottom2 = top2 + LINE_THICK;
canvas.drawRect(left2, top2, right2, bottom2, gridPaint);
}
}
private void drawElement(Canvas canvas, char c, int x, int y) {
if (c == 'O') {
float cx = (elementWidth * x) + elementWidth / 2;
float cy = (elementHeight * y) + elementHeight / 2;
canvas.drawCircle(cx, cy, Math.min(elementWidth, elementHeight) / 2 - ELEMENT_MARGIN * 2, oPaint);
} else if (c == 'X') {
float startX = (elementWidth * x) + ELEMENT_MARGIN;
float startY = (elementHeight * y) + ELEMENT_MARGIN;
float endX = startX + elementWidth - ELEMENT_MARGIN * 2;
float endY = startY + elementHeight - ELEMENT_MARGIN;
canvas.drawLine(startX, startY, endX, endY, xPaint);
float startX2 = (elementWidth * (x + 1)) - ELEMENT_MARGIN;
float startY2 = (elementHeight * y) + ELEMENT_MARGIN;
float endX2 = startX2 - elementWidth + ELEMENT_MARGIN * 2;
float endY2 = startY2 + elementHeight - ELEMENT_MARGIN;
canvas.drawLine(startX2, startY2, endX2, endY2, xPaint);
}
}
}
Use the BoardView
in res/layout/activity_main.xml
:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:orientation="vertical"
tools:context="com.learning.tictactoegame.MainActivity">
<com.learning.tictactoegame.BoardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/board"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</RelativeLayout>
Add BoardView
to MainActivity
:
package com.learning.tictactoegame;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
private BoardView boardView;
private GameEngine gameEngine;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
boardView = (BoardView) findViewById(R.id.board);
gameEngine = new GameEngine();
boardView.setGameEngine(gameEngine);
boardView.setMainActivity(this);
}
public void gameEnded(char c) {
String msg = (c == 'T') ? "Game Ended in a Tie" : "Game Ended - " + c + " is the winner";
new AlertDialog.Builder(this).setTitle("Tic Tac Toe")
.setMessage(msg)
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
newGame();
}
})
.show();
}
private void newGame() {
gameEngine.newGame();
boardView.invalidate();
}
}