This commit is contained in:
louiscklaw
2025-02-01 01:58:47 +08:00
parent b3da7aaef5
commit 04dbefcbaf
1259 changed files with 280657 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
# ITP4501 Project
In this assignment, you are required to develop an Android Application to play a Tic-Tac-Toe Game. This app will also record the result and corresponding time required to complete a game and use charts to show the history records.
You can use following link to know how to play a Tic-Tac-Teo game:
[https://en.wikipedia.org/wiki/Tic-tac-toe](https://en.wikipedia.org/wiki/Tic-tac-toe)
## Functional Requirements
Listed below are the basic requirements of your application. You need to refer to the Local Database section for the database schema.
1. A main activity which contains a main menu for players to choose. The four main functions are: Play, Game Ranking, Your Records and Close.
2. When players touch the "Play" button on the main menu, they start to play the game with CPU. Players will mark an 'O' on an available button which does not contain any mark ('O' or 'X') by touching it. After players touch on an available button, CPU will RANDOMLY to mark a 'X' on an available button. To simplify your workload, you are not required to write an AI for CPU to win the game.
When players or CPU who succeed in placing three of their marks in a horizontal, vertical, or diagonal row is the winner. Your app will save the record of this game to the GamesLog table in a local database and then your app will show a "Continue" for players to restart the game.
3. When players touch the "Game Ranking" button on the main menu, your app will download a JSON from your own api server. You MUST use a ListView to show all the records in the JSON string.
4. When players touch the "Your Records" button on the main menu, Your app will load the records in the GamesLog table from your local database. You MUST use a ListView to show all these records. Your app will provide a button "Show in Pie Chart" which let players to show winning status (Win, Lose, Draw) by using a Pie Chart.
Note: You are encouraged to design and implement extra features. 10% of the total mark will be allocated on such additional functions. Refer to section 7 Marking Guidelines for more details.
## Local Database
The database scheme described here is an extremely simple one. Many fields are intended not to be included in order to reduce the complexity of this assignment. You are free to add columns and tables to the database to fit for your own needs.
```
GamesLog (gameID, playDate, playTime, duration, winningStatus)
```
## Ranking JSON Server
You can obtain a ranking list from your own api server and the data returned is in JSON format.
The JSON string returned is shown below:
```
[{"Name":"Kenny Lam","Duration":104},
{"Name":"Peter Kwong","Duration":25},
{"Name":"John Chan","Duration":38},
{"Name":"Johnny Kwong","Duration":117},
{"Name":"Mary Lam","Duration":23},
{"Name":"David Wong","Duration":49},
{"Name":"Alan Ma","Duration":18},
{"Name":"Carrie Lam","Duration":68},
{"Name":"Chris Lam","Duration":93},
{"Name":"Mary Cheung","Duration":78}]
```
You need to sort the JSON data by using duration in ascending order to obtain the ranking.
### Marking Guidelines
You project will be assessed according to the items below.
➢ Database initialisation.
➢ Level of completion.
➢ Correctness.
➢ UI design (no mark will be given if you are using the same design in this document).
➢ Program design and implementation.
➢ Program style and comments.
➢ Driving Question: How can an organization get benefit from a central computerized management system by using a mobile app?
 Briefly discuss how ITP4510, ITP4522 and ITP4915M modules help you to finish this assignment.

View File

@@ -0,0 +1,178 @@
package com.game.tictacteo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import com.game.tictacteo.engine.Game;
import com.game.tictacteo.engine.GameBoard;
import com.game.tictacteo.engine.GameBoardPlacedException;
import com.game.tictacteo.engine.GameStatus;
import java.util.HashMap;
public class GameActivity extends AppCompatActivity implements View.OnClickListener {
private HashMap<Integer, LocateButton> gameBoardMap;
private Game game;
private ImageView imGameMes;
private final static byte USER_PLAYER = 1;
private final static byte AI_PLAYER = 2;
//private ArrayList<Integer> lockedButton;
private ImageView imDisPlayer;
private ImageView imDisAI;
private Button btnContinue;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game);
//this.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
imGameMes = (ImageView) findViewById(R.id.txtGameMes);
imDisPlayer = (ImageView) findViewById(R.id.imDisplayPlayer);
imDisAI = (ImageView) findViewById(R.id.imDisplayAI);
btnContinue = (Button) findViewById(R.id.btnContinue);
btnContinue.setVisibility(View.GONE);
// Close button
btnContinue.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
startActivity(getIntent());
}
});
// Button Map to reduce the runtime
gameBoardMap = new HashMap<Integer, LocateButton>();
for(int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++){
String buttonID = "btn_"+i+"_"+j;
int resourceID = getResources().getIdentifier(buttonID, "id", getPackageName());
Button button = (Button) findViewById(resourceID);
button.setOnClickListener(this);
Log.i("gameAc-onCreate", "refesh");
gameBoardMap.put(resourceID, new LocateButton(button, new int[]{i, j}));
}
}
Log.i("gameAc", "start");
game = new Game(GameActivity.this);
//lockedButton = new ArrayList<Integer>();
}
// refesh the game board
public void refesh(byte[][] board){
Log.i("gameAc", "refesh");
for(int i = 0; i < board.length; i++){
for(int j = 0; j < board[i].length; j++){
String mapKey = "btn_"+i+"_"+j;
int resourceID = getResources().getIdentifier(mapKey, "id", getPackageName());
// Get button from hashmap
Button button = gameBoardMap.get(resourceID).button;
// set button image and disable clicked button
if(board[i][j] == USER_PLAYER){
button.setBackgroundResource(R.drawable.ic_o);
button.setClickable(false);
}else if (board[i][j] == AI_PLAYER){
button.setBackgroundResource(R.drawable.ic_x);
button.setClickable(false);
}else{
button.setBackgroundResource(R.drawable.game_board_pos);
button.setClickable(true);
}
}
}
}
// disable all button
public void lockAllButton(){
for(LocateButton lb: gameBoardMap.values())
lb.button.setClickable(false);
}
// public void unlockAllButton(){
// for(Integer key: gameBoardMap.keySet()){
// Button button = gameBoardMap.get(key).button;
// if(lockedButton.contains(key))
// button.setClickable(false);
// else
// button.setClickable(true);
//
// }
// }
@Override
public void onClick(View view) {
Log.i("gameAc", view.getId() +"");
if (gameBoardMap.containsKey(view.getId())) {
LocateButton lb = gameBoardMap.get(view.getId());
// Player place
lockAllButton();
// set clicked button UI setting
lb.button.setBackgroundResource(R.drawable.ic_o);
imGameMes.setImageResource(R.drawable.game_mes_2);
imDisPlayer.setBackgroundResource(0);
imDisAI.setBackgroundResource(R.drawable.btnranking);
//lockedButton.add(view.getId());
try {
// Place the index
GameStatus gs = game.player1Place(lb.locate[0], lb.locate[1]);
this.refesh(gs.board);
imGameMes.setImageResource(R.drawable.game_mes_1);
imDisAI.setBackgroundResource(0);
// Check returned game statue and update the UI
if(gs.status == GameBoard.Status.CONTINUE) {
imDisPlayer.setBackgroundResource(R.drawable.btnranking);
//unlockAllButton();
}else{
if (gs.status == GameBoard.Status.DRAW){
imDisPlayer.setBackgroundResource(R.drawable.btnranking);
imGameMes.setImageResource(R.drawable.game_dis_draw);
}else if (gs.status == GameBoard.Status.PLAYER1_WIN){
imGameMes.setImageResource(R.drawable.game_dis_win);
}else if(gs.status == GameBoard.Status.PLAYER2_WIN) {
imDisPlayer.setBackgroundResource(0);
imDisAI.setBackgroundResource(R.drawable.btnranking);
imGameMes.setImageResource(R.drawable.game_dis_lose);
}
btnContinue.setVisibility(View.VISIBLE);
}
} catch (GameBoardPlacedException e) {
e.printStackTrace();
}
}
}
}
class LocateButton {
Button button;
int[] locate; // row, col index
LocateButton(Button button, int[]locate){
this.button = button;
this.locate = locate;
}
}

View File

@@ -0,0 +1,32 @@
package com.game.tictacteo;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.widget.TextView;
public class InitActivity extends AppCompatActivity {
private TextView tvLoadingMes;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_init);
// hidden Action bar
// getSupportActionBar().hide();
tvLoadingMes = (TextView) findViewById(R.id.tvLoadingMes);
// For Some setup things
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Intent intent = new Intent(InitActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
}, 2000);
}
}

View File

@@ -0,0 +1,40 @@
package com.game.tictacteo;
import androidx.appcompat.app.AppCompatActivity;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.widget.TextView;
public class LoadingDialog {
Context context;
Dialog dialog;
TextView tvLoadingDailogMes;
public LoadingDialog(Context context){
this.context = context;
dialog = new Dialog(context);
dialog.setContentView(R.layout.activity_loading_dialog);
tvLoadingDailogMes = (TextView) dialog.findViewById(R.id.tvLoadingDialogMes);
}
public void show(String message){
dialog.getWindow().setBackgroundDrawable(new ColorDrawable((Color.TRANSPARENT)));
// Disable click outside then close the loading dialog
dialog.setCanceledOnTouchOutside(false);
// set Header
tvLoadingDailogMes.setText(message);
dialog.create();
dialog.show();
}
public void hide(){
dialog.dismiss();
}
}

View File

@@ -0,0 +1,67 @@
package com.game.tictacteo;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
Button btnStartGame;
Button btnRanking;
Button btnRecords;
Button btnClose;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnStartGame = (Button) findViewById(R.id.btnPlay);
btnRanking = (Button) findViewById(R.id.btnRanking);
btnRecords = (Button) findViewById(R.id.btnRec);
btnClose = (Button) findViewById(R.id.btnClose);
// Start game
btnStartGame.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent( MainActivity.this, GameActivity.class);
startActivity(intent);
}
});
// Game Ranking
btnRanking.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent( MainActivity.this, RankingActivity.class);
startActivity(intent);
}
});
// Your Records
btnRecords.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent( MainActivity.this, RecordActivity.class);
startActivity(intent);
}
});
// Close
btnClose.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
System.out.println("click");
finish();
}
});
}
}

View File

@@ -0,0 +1,97 @@
package com.game.tictacteo;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import com.game.tictacteo.model.UserRanking;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
public class RankingActivity extends AppCompatActivity {
private Button btnBack;
private final static String API_ENDPOINT = "http://192.168.76.3/ranking_api.php";
private LoadingDialog ld;
private ArrayList<UserRanking> users;
private RecyclerView rvRankingListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ranking);
// Load Dailog
ld = new LoadingDialog(this);
users = new ArrayList<UserRanking>();
btnBack = (Button) findViewById(R.id.btnBack);
rvRankingListView = (RecyclerView) findViewById(R.id.rvRankingList);
btnBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
// show loading dialog
ld.show("fetching...");
new Thread(() -> {
try{
// fetch API
URL url = new URL(API_ENDPOINT);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
InputStream inputStream = connection.getInputStream();
BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
String data = buffer.readLine();
StringBuffer json = new StringBuffer();
while(data != null){
json.append(data);
data = buffer.readLine();
}
// JSON List to ArrayList
JSONArray jsonArray = new JSONArray(String.valueOf(json));
for(int i = 0; i < jsonArray.length(); i++){
JSONObject jsonObj = jsonArray.getJSONObject(i);
String name = jsonObj.getString("Name");
int duration = jsonObj.getInt("Duration");
users.add(new UserRanking(name, duration));
Log.i("ranking", "add" + name);
}
runOnUiThread(() -> {
ld.hide();
UserRankingAdapter rankingAdapter = new UserRankingAdapter(this, users);
rvRankingListView.setAdapter(rankingAdapter);
rvRankingListView.setLayoutManager(new LinearLayoutManager(RankingActivity.this));
});
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
}
).start();
}
}

View File

@@ -0,0 +1,148 @@
package com.game.tictacteo;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import com.game.tictacteo.localDB.GameLogDB;
import com.game.tictacteo.model.GameLog;
import java.util.ArrayList;
public class RecordActivity extends AppCompatActivity {
private Button btnRecordBack;
private SurfaceView winStatePieChartView;
private SurfaceHolder holder;
private RecyclerView rvGameLogList;
private ArrayList<GameLog> logs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_record);
logs = GameLogDB.getInstance(this).readAllData();
winStatePieChartView = (SurfaceView) findViewById(R.id.sfwinStatePieChartView);
// For get Canvas in SurfaceView
holder = winStatePieChartView.getHolder();
if(logs.size() > 0){
// If not callback will throw error due to SurfaceView.canvas is not created in onCreate()
holder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
float[] perc = calPercentage(logs);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeWidth(4);
Canvas canvas = holder.lockCanvas();
RectF rectF = new RectF(225,50,winStatePieChartView.getWidth()-225,winStatePieChartView.getHeight()-50);
// draw the lose pie
paint.setColor(Color.RED);
canvas.drawArc(rectF, 0, 360*perc[0], true, paint);
// draw the win pie
paint.setColor(Color.GREEN);
canvas.drawArc(rectF, 360*perc[0], 360*perc[1], true, paint);
// draw the draw pie
paint.setColor(Color.YELLOW);
canvas.drawArc(rectF, 360*perc[0]+360*perc[1], 360*perc[2], true, paint);
// setup text paint
paint.setStrokeWidth(0);
paint.setTextSize(30);
// write Draw Text
canvas.drawRect(winStatePieChartView.getWidth()-160, winStatePieChartView.getHeight()-200, winStatePieChartView.getWidth()-135, winStatePieChartView.getHeight()-175, paint);
canvas.drawText("DRAW", winStatePieChartView.getWidth()-125, winStatePieChartView.getHeight()-177, paint);
// write Lose Text
paint.setColor(Color.RED);
canvas.drawRect(winStatePieChartView.getWidth()-160, winStatePieChartView.getHeight()-150, winStatePieChartView.getWidth()-135, winStatePieChartView.getHeight()-125, paint);
canvas.drawText("LOSE", winStatePieChartView.getWidth()-125, winStatePieChartView.getHeight()-127, paint);
// write Win Text
paint.setColor(Color.GREEN);
canvas.drawRect(winStatePieChartView.getWidth()-160, winStatePieChartView.getHeight()-100, winStatePieChartView.getWidth()-135, winStatePieChartView.getHeight()-75, paint);
canvas.drawText("WIN", winStatePieChartView.getWidth()-125, winStatePieChartView.getHeight()-77, paint);
holder.unlockCanvasAndPost(canvas);
//holder.lockCanvas(new Rect(0, 0, 0, 0));
//holder.unlockCanvasAndPost(canvas);
}
private float[] calPercentage(ArrayList<GameLog> data){
int[] count = new int[3]; // 0 -> lose, 1 -> win, 2 -> draw
int total = 0;
for(GameLog gl: data ) {
count[gl.getWinningStatus()]++;
total++;
}
Log.i("prc1", count[0]*1.0f/total+"");
Log.i("prc2", count[1]*1.0f/total+"");
Log.i("prc3", count[2]*1.0f/total+"");
return new float[]{count[0]*1.0f/total, count[1]*1.0f/total, count[2]*1.0f/total};
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
}
});
}
rvGameLogList = (RecyclerView) findViewById(R.id.rvGameLogList);
btnRecordBack = (Button) findViewById(R.id.btnRecordBack);
btnRecordBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
// create adapter for attached the log list
UserRecordAdapter rankingAdapter = new UserRecordAdapter(this, logs);
rvGameLogList.setAdapter(rankingAdapter);
rvGameLogList.setLayoutManager(new LinearLayoutManager(this));
}
}

View File

@@ -0,0 +1,59 @@
package com.game.tictacteo;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.game.tictacteo.model.UserRanking;
import java.util.ArrayList;
public class UserRankingAdapter extends RecyclerView.Adapter<UserRankingAdapter.Viewholder> {
private Context context;
private ArrayList<UserRanking> users;
// Constructor
public UserRankingAdapter(Context context, ArrayList<UserRanking> users) {
this.context = context;
this.users = users;
}
@NonNull
@Override
public UserRankingAdapter.Viewholder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_ranking_card_row, parent, false);
return new Viewholder(view);
}
// Set Text for each row
@Override
public void onBindViewHolder(@NonNull UserRankingAdapter.Viewholder holder, int position) {
UserRanking user = users.get(position);
holder.tvRankingNo.setText(String.valueOf(position+1));
holder.tvRankingName.setText("Name: " + user.getName());
holder.tvRankingDuration.setText("Duration: " + String.valueOf(user.getDuration()));
}
@Override
public int getItemCount() {
return users.size();
}
// Init find Element ID;
public class Viewholder extends RecyclerView.ViewHolder {
private TextView tvRankingNo, tvRankingName, tvRankingDuration;
public Viewholder(@NonNull View itemView) {
super(itemView);
tvRankingNo = itemView.findViewById(R.id.tvRecordDateTime);
tvRankingName = itemView.findViewById(R.id.tvRecordState);
tvRankingDuration = itemView.findViewById(R.id.tvRecordDuration);
}
}
}

View File

@@ -0,0 +1,70 @@
package com.game.tictacteo;
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.game.tictacteo.model.GameLog;
import com.game.tictacteo.model.UserRanking;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
public class UserRecordAdapter extends RecyclerView.Adapter<UserRecordAdapter.Viewholder> {
private Context context;
private ArrayList<GameLog> logs;
public UserRecordAdapter(Context context, ArrayList<GameLog> logs) {
this.context = context;
this.logs = logs;
}
@NonNull
@Override
public UserRecordAdapter.Viewholder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_record_card_row, parent, false);
return new Viewholder(view);
}
@Override
public void onBindViewHolder(@NonNull UserRecordAdapter.Viewholder holder, int position) {
GameLog log = logs.get(position);
SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat timeFormat = new SimpleDateFormat("hh:mm aa");
holder.tvRecordDateTime.setText(dateFormat.format(log.getPlayDate()) + " " + timeFormat.format(log.getPlayTime()));
holder.tvRecordState.setText("WinState: " + log.winStateToString());
if(log.getWinningStatus() == 0)
holder.tvRecordState.setTextColor(Color.RED);
else if (log.getWinningStatus() == 1)
holder.tvRecordState.setTextColor(Color.GREEN);
else
holder.tvRecordState.setTextColor(Color.BLACK);
holder.tvRecordDuration.setText("Duration: " + String.valueOf(log.getDuration()) + " sec");
}
@Override
public int getItemCount() {
return logs.size();
}
public class Viewholder extends RecyclerView.ViewHolder {
private TextView tvRecordDateTime, tvRecordState, tvRecordDuration;
public Viewholder(@NonNull View itemView) {
super(itemView);
tvRecordDateTime = itemView.findViewById(R.id.tvRecordDateTime);
tvRecordState = itemView.findViewById(R.id.tvRecordState);
tvRecordDuration = itemView.findViewById(R.id.tvRecordDuration);
}
}
}

View File

@@ -0,0 +1,68 @@
package com.game.tictacteo.engine;
import android.content.Context;
import android.util.Log;
import com.game.tictacteo.localDB.GameLogDB;
import com.game.tictacteo.model.GameLog;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Date;
public class Game {
private Timestamp startTime;
private Timestamp endTime;
private GameBoard board;
private Context ctx;
public Game(Context ctx) {
startTime = new Timestamp(System.currentTimeMillis());
board = new GameBoard();
this.ctx = ctx;
}
// User move
public GameStatus player1Place(int row, int col) throws GameBoardPlacedException {
GameStatus gs = place(row, col, 1);
checkGameStatus(gs); // check status if won not need AI move
if (gs.status == GameBoard.Status.CONTINUE) {
gs = aiPlayerPlace();
checkGameStatus(gs); // check status
}
return gs;
}
private GameStatus place(int row, int col, int player) throws GameBoardPlacedException {
if (player == 1)
return this.board.placePlayer1(row, col);
else if (player == 2)
return this.board.placePlayer2(row, col);
return null;
}
private void checkGameStatus(GameStatus gs) {
// check status if Player1 or Player2 win need to insert the log to db
if (gs.status != GameBoard.Status.CONTINUE) {
endTime = new Timestamp(System.currentTimeMillis());
int duration = (int) ((endTime.getTime() - startTime.getTime()) / 1000);
int winingStatus = ((gs.status == GameBoard.Status.DRAW) ? 2 : (gs.status == GameBoard.Status.PLAYER2_WIN) ? 0 : 1);
GameLogDB.getInstance(this.ctx).addLog(new GameLog(new Date(endTime.getTime()), new Time(endTime.getTime()), duration, winingStatus));
}
}
// AI move
private GameStatus aiPlayerPlace() throws GameBoardPlacedException {
Log.i("game-OBJ-AI", "aiPLayerPlace");
int[] move = GameAI.findBestMove(board.getBoard());
return place(move[0], move[1], 2);
}
}

View File

@@ -0,0 +1,151 @@
package com.game.tictacteo.engine;
import android.util.Log;
public class GameAI {
static final byte player = 2;
static final byte opponent = 1;
private static Boolean isMovesLeft(byte board[][]) {
for (int i = 0; i < board.length; i++)
for (int j = 0; j < board[i].length; j++)
if (board[i][j] == 0)
return true;
return false;
}
private static int evaluate(byte[][] board) {
// Checking for Rows is player or opponent victory.
for (int row = 0; row < board.length; row++) {
if (board[row][0] == board[row][1] &&
board[row][1] == board[row][2]) {
if (board[row][0] == player)
return +10;
else if (board[row][0] == opponent)
return -10;
}
}
for (int col = 0; col < board[0].length; col++) {
if (board[0][col] == board[1][col] &&
board[1][col] == board[2][col]) {
if (board[0][col] == player)
return +10;
else if (board[0][col] == opponent)
return -10;
}
}
if (board[0][0] == board[1][1] && board[1][1] == board[2][2]) {
if (board[0][0] == player)
return +10;
else if (board[0][0] == opponent)
return -10;
}
if (board[0][2] == board[1][1] && board[1][1] == board[2][0]) {
if (board[0][2] == player)
return +10;
else if (board[0][2] == opponent)
return -10;
}
// Else if none of them have won then return 0
return 0;
}
private static int minimax(byte board[][], int depth, Boolean isMax) {
int score = evaluate(board);
// Evaluated score if Maximizer has won the game
if (score == 10)
return score;
if (score == -10)
return score;
// no winner then it is a tie if false
if (isMovesLeft(board) == false)
return 0;
if (isMax) {
int best = -1000;
// Traverse all cells
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
// Check cell is empty
if (board[i][j] == 0) {
// Make the move
board[i][j] = player;
// calculate minimax recursively and choose
best = Math.max(best, minimax(board, depth + 1, !isMax));
// Undo move
board[i][j] = 0;
}
}
}
return best;
}else {
int best = 1000;
// Traverse all cells
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
// Check cell is empty
if (board[i][j] == 0) {
// Make the move
board[i][j] = opponent;
// calculate minimax recursively and choose
best = Math.min(best, minimax(board,
depth + 1, !isMax));
// Undo move
board[i][j] = 0;
}
}
}
return best;
}
}
public static int[] findBestMove(byte board[][]) {
int bestVal = -1000;
int[] bestMove = new int[2];
bestMove[0] = -1;
bestMove[1] = -1;
// evaluate minimax func
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
// Check cell is empty
if (board[i][j] == 0) {
// Make the move
board[i][j] = player;
// compute evaluation function
int moveVal = minimax(board, 0, false);
// Undo the move
board[i][j] = 0;
// If the value of the current move is higher then best value, then update
if (moveVal > bestVal) {
bestMove[0] = i;
bestMove[1] = j;
bestVal = moveVal;
}
}
}
}
return bestMove;
}
}

View File

@@ -0,0 +1,139 @@
package com.game.tictacteo.engine;
import android.util.Log;
public class GameBoard {
private final static int ROW = 3; // Game Board ROW
private final static int COL = 3; // Game Board COL
private byte[][] board;
private short step;
private short winner; // Winner
public enum Status {
CONTINUE,
PLAYER1_WIN,
PLAYER2_WIN,
DRAW
}
// 0 0 0
// 0 0 0
// 0 0 0
GameBoard() {
board = new byte[ROW][COL];
step = 0;
winner = -1;
}
public byte[][] getBoard() {
return board;
}
public GameStatus placePlayer1(int row, int col) throws GameBoardPlacedException {
return place(row, col, 1);
}
public GameStatus placePlayer2(int row, int col) throws GameBoardPlacedException{
return place(row, col, 2);
}
private GameStatus place(int row, int col, int player) throws GameBoardPlacedException {
if(board[row][col] != 0)
throw new GameBoardPlacedException("The position is placed: " + row + " " + col);
board[row][col] = (byte) player;
step++; // count step for game
return genGameStatus();
}
private GameStatus genGameStatus(){
// return current game board status
// step == board.length mean all is placed
if(step == board[0].length * board.length)
return new GameStatus(Status.DRAW, this.board);
// check has winner?
if(winner == -1 && !checkWin())
return new GameStatus(Status.CONTINUE, this.board);
else
return new GameStatus((this.winner == 1) ? Status.PLAYER1_WIN: Status.PLAYER2_WIN, this.board);
}
private boolean checkWin(){
return (checkRows() || checkColumns() || checkDiagonals());
}
private boolean checkRows() {
for (int i = 0; i < board.length; i++) {
int count = 1;
for (int j = 1; j < board[i].length; j++) {
// compare other col is same?
if (board[i][0] != 0 && board[i][0] == board[i][j])
count++;
else
break;
}
if (count == board[i].length){
this.winner = board[i][0];
return true;
}
}
return false;
}
private boolean checkColumns() {
for (int i = 0; i < board[0].length; i++) {
int count = 1;
for (int j = 1; j < board.length; j++) {
// compare other col is same?
if (board[0][i] != 0 && board[0][i] == board[j][i])
count++;
else
break;
}
if (count == board.length) {
this.winner = board[0][i];
return true;
}
}
return false;
}
// checkDiagonals
private boolean checkDiagonals() {
int countX = 1; // for count left-top to right-bottom
int countY = 1; // for count right-top to left-bottom
for (int j = 1; j < board.length; j++) {
// compare other col is same?
if (board[0][0] != 0 && board[0][0] == board[j][j])
countX++;
if (board[0][board[0].length - 1] != 0 && board[0][board[0].length - 1] == board[j][board[0].length - 1 - j])
countY++;
}
if (countX == board.length) {
this.winner = board[0][0];
return true;
}
if (countY == board.length) {
this.winner = board[0][board[0].length-1];
return true;
}
return false;
}
}

View File

@@ -0,0 +1,9 @@
package com.game.tictacteo.engine;
public class GameBoardPlacedException extends Exception{
public GameBoardPlacedException(String mes){
super(mes);
}
}

View File

@@ -0,0 +1,12 @@
package com.game.tictacteo.engine;
public class GameStatus {
public GameBoard.Status status;
public byte[][] board;
public GameStatus(GameBoard.Status status, byte[][] board){
this.status = status;
this.board = board;
}
}

View File

@@ -0,0 +1,106 @@
package com.game.tictacteo.localDB;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import com.game.tictacteo.model.GameLog;
import java.sql.Date;
import java.sql.Time;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
public class GameLogDB extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "GameLog.db";
private static final String GAMESLOG_TABLE_NAME = "GamesLog";
private static final int DATABASE_VERSION = 1;
// Singleton pattern
private static GameLogDB instance = null;
public static GameLogDB getInstance(Context ctx) {
if (instance == null)
instance = new GameLogDB(ctx.getApplicationContext());
return instance;
}
public GameLogDB(Context context) {
// applcation_context, db_name, factory, db_version
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
System.out.println("Database creating");
String sql = "CREATE TABLE " + GAMESLOG_TABLE_NAME + " ( " +
"gameID INTEGER PRIMARY KEY AUTOINCREMENT, " +
"playDate TEXT NOT NULL, " +
"playTime TEXT NOT NULL, " +
"duration INTEGER NOT NULL, " +
"winningStatus INTEGER NOT NULL);";
db.execSQL(sql);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + GAMESLOG_TABLE_NAME + ";");
onCreate(db);
}
// Add log
public void addLog(GameLog log) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat timeFormat = new SimpleDateFormat("hh:mm aa");
values.put("playDate", dateFormat.format(log.getPlayDate()));
values.put("playTime", timeFormat.format(log.getPlayTime()));
values.put("duration", log.getDuration());
values.put("winningStatus", log.getWinningStatus());
db.insert(GAMESLOG_TABLE_NAME, null, values);
db.close();
}
// select all data
public ArrayList<GameLog> readAllData() {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(GAMESLOG_TABLE_NAME, new String[]{"playDate", "playTime", "duration", "winningStatus"}, null, null, null, null, "gameId");
ArrayList<GameLog> data = new ArrayList<GameLog>();
// Time formatter
SimpleDateFormat timeFormat = new SimpleDateFormat("hh:mm aa");
try {
while (cursor.moveToNext()) {
// yyyy-MM-dd striing to date object
Date playDate = Date.valueOf(cursor.getString(0));
Log.i("db", cursor.getString(1));
Time playTime = new Time(timeFormat.parse(cursor.getString(1)).getTime());
int duration = cursor.getInt(2);
short winningStatus = cursor.getShort(3);
data.add(new GameLog(playDate, playTime, duration, winningStatus));
}
} catch (ParseException e) {
e.printStackTrace();
}
db.close();
return data;
}
}