零 成品展示
功能
- 轮流落子,提示落子方,判定输赢
- 胜负分晓或棋盘满后toast提示需要重新开始
- 播放/暂停背景音乐
- 按下按钮重新开始游戏
一 源码
MainActivity.java代码
package com.example.tictactoe;
import android.app.*;
import androidx.appcompat.app.AppCompatActivity;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private TextView info;
private int moveNow = 0;
boolean musicOn = true;
private enum StateEnum {
RedCircle,
GreenCross,
Blank
}
private ImageView[] BoardList = new ImageView[9];
private StateEnum[][] BoardState = new StateEnum[3][3];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
info = findViewById(R.id.infoview);
setList();
initBoard();
setOnClickListener();
}
private void click(View v) {
if (!hasWon() && !hasDrawn()) {
//RedCircle First
makeMove(v, moveNow % 2 == 0 ? StateEnum.RedCircle : StateEnum.GreenCross);
} else {
Toast.makeText(getApplicationContext(), "please restart the game", Toast.LENGTH_SHORT).show();
}
}
//成功落子返回true
private boolean makeMove(View v, StateEnum s) {
try {
int index = findIndex((ImageView) v);
if (index == -1) {
return false;
}
if (BoardState[index / 3][index % 3] == StateEnum.Blank) {
BoardState[index / 3][index % 3] = s;
moveNow++;
if (s == StateEnum.GreenCross) {
((ImageView) v).setBackgroundResource(R.drawable.cross);
info.setText("Red Hand");
info.setTextColor((Color.RED));
} else {
//red cross
((ImageView) v).setBackgroundResource(R.drawable.circle);
info.setText("Green Hand");
info.setTextColor(Color.GREEN);
}
if(hasWon()){
if(moveNow%2==0){
info.setText("Green Hand Won");
info.setTextColor(Color.GREEN);
}
else{
info.setText("Red Hand Won");
info.setTextColor((Color.RED));
}
}
else if(hasDrawn()){
info.setText("Draw");
info.setTextColor((Color.BLACK));
}
return true;
}
//若棋盘被占,重新落子
return false;
} catch (Exception e) {
return false;
}
}
//初始化ImageView-下标词典
private void setList() {
String[] names = new String[9];
for (int i = 1; i < 4; i++) {
for (int j = 1; j < 4; j++) {
names[(i - 1) * 3 + j - 1] = "image" + i + j;
}
}
Resources res = getResources();
for (int i = 0; i < 9; i++) {
int id = res.getIdentifier(names[i], "id", getPackageName());
BoardList[i] = findViewById(id);
}
}
//返回view的下标
private int findIndex(ImageView v) {
for (int i = 0; i < 9; i++) {
if (BoardList[i].equals(v)) {
return i;
}
}
return -1;
}
private boolean hasWon() {
int[][] winningpattern = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, {0, 4, 8}, {2, 4, 6}};
StateEnum[] players = {StateEnum.GreenCross, StateEnum.RedCircle};
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 2; j++) {
StateEnum player = players[j];
if (BoardState[winningpattern[i][0] / 3][winningpattern[i][0] % 3] == player
&& BoardState[winningpattern[i][1] / 3][winningpattern[i][1] % 3] == player
&& BoardState[winningpattern[i][2] / 3][winningpattern[i][2] % 3] == player) {
return true;
}
}
}
return false;
}
private boolean hasDrawn() {
int cnt = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (BoardState[i][j] == StateEnum.Blank) {
cnt++;
}
}
}
if (cnt < 1) {
return true;
}
return false;
}
private void setOnClickListener() {
for (int i = 0; i < 9; i++) {
ImageView v = BoardList[i];
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
click(v);
}
});
}
Button buttonRestart = findViewById(R.id.buttonRestart);
buttonRestart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
initBoard();
moveNow = 0;
}
});
ImageButton musicButton = findViewById(R.id.musicButton);
musicButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(!musicOn){
onResume();
}
else{
onPause();
}
musicOn = !musicOn;
}
});
}
private void initBoard() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
BoardState[i][j] = StateEnum.Blank;
BoardList[i*3+j].setBackgroundResource(R.drawable.blank);
}
}
info.setText("Red Hand");
info.setTextColor((Color.RED));
}
@Override
protected void onPause() {
super.onPause();
MusicServer.stop(this);
}
@Override
protected void onResume() {
super.onResume();
MusicServer.play(this, R.raw.song);
}
}
MusicServer.java
package com.example.tictactoe;
import android.content.Context;
import android.media.MediaPlayer;
public class MusicServer{
private static MediaPlayer mp =null;
public static void play(Context context, int resource){
stop(context);
mp = MediaPlayer.create(context, resource);
mp.setLooping(true);
mp.start();
}
public static void stop(Context context) {
if(mp!= null){
mp.stop();
mp.release();
mp = null;
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
tools:context=".MainActivity"
android:orientation="vertical"
android:weightSum="100"
android:background="#ccffff"
>
<RelativeLayout
android:layout_width = "match_parent"
android:layout_height = "0dp"
android:layout_weight = "20"
>
<ImageButton
android:id="@+id/musicButton"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:background="@drawable/music" />
<TextView
android:id="@+id/infoview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/musicButton"
android:text="Now: Red Hand"
android:textColor="#ff0000"
android:textSize="30sp" />
</RelativeLayout>
<RelativeLayout
android:layout_width = "match_parent"
android:layout_height = "0dp"
android:layout_weight = "50"
>
<TableLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#70f3ff"
android:alpha="0.5">
<TableRow
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight = "1">
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/blank"
android:id="@+id/image11"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/blank"
android:id="@+id/image12"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/blank"
android:id="@+id/image13"/>
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight = "1">
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/blank"
android:id="@+id/image21"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/blank"
android:id="@+id/image22"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/blank"
android:id="@+id/image23"/>
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/blank"
android:id="@+id/image31"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/blank"
android:id="@+id/image32"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/blank"
android:id="@+id/image33"/>
</TableRow>
</TableLayout>
</RelativeLayout>
<Button
android:id="@+id/buttonRestart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Resume" />
</LinearLayout>
二 文件结构
红圈部分需要自行添加。其中blank.png为棋盘空格子图片。circle.png为和cross.png为不同棋子的图片,根据井字棋的传统,选择一圈一叉。music.png为音乐图标。raw/song.mp3是背景音乐文件,建议选择压缩后的比较小的文件,我的文件尺寸为1798KB。
可以在以下链接下载,也可以使用其他合适的图片与音频,但注意项目中的文件名与代码中的变量要一一对应。
三 解析
对Android开发还不了解的友友看这里的前置知识: Android开发手把手入门精讲1:英里公里转换器
这里讲解XML文件和MainActivity.java文件。MusicServer.java文件比较简单,省略。
XML解析
这是Design布局的效果。可以看到,界面垂直方向被一个LinearLayout上分为上、中、下三块,每块是一个RelativeLayout。中间的RelativeLayout均分为3个TableRow,每个TableRow均分为3个ImageView。
(1)均分宽度
给同一行的3个ImageView添加如下属性能让三者均分父容器的宽度
android:layout_width="wrap_content"
(2)按比例分配高度
给LinearLayout添加如下属性
android:orientation="vertical"
android:weightSum="100"
再给它的孩子容器,RelativeLayout,添加如下属性
android:layout_height = "0dp"
android:layout_weight = "20"
就可以让该RelativeLayout占总高度的20/100。
MainActivity.java解析
private enum StateEnum {
RedCircle,
GreenCross,
Blank
}
StateEnum定义了单个棋盘格子的三种状态:Blank表示未落子的空格,RedCircel与GreenCross则是双方的棋子。
private StateEnum[][] BoardState = new StateEnum[3][3];
BoardState二维数组的保存3*3棋盘下每个格子的落子情况。开始游戏以及重新开始游戏时需要全部初始化为Blank。
private ImageView[] BoardList = new ImageView[9];
//初始化ImageView-下标词典
private void setList() {
String[] names = new String[9];
for (int i = 1; i < 4; i++) {
for (int j = 1; j < 4; j++) {
names[(i - 1) * 3 + j - 1] = "image" + i + j;
}
}
Resources res = getResources();
for (int i = 0; i < 9; i++) {
int id = res.getIdentifier(names[i], "id", getPackageName());
BoardList[i] = findViewById(id);
}
}
//返回view的下标
private int findIndex(ImageView v) {
for (int i = 0; i < 9; i++) {
if (BoardList[i].equals(v)) {
return i;
}
}
return -1;
}
BoardList里的9个成员对应组成棋盘的9个格子,类型采用ImageView是为了显示未落子或落某方棋子的画面。
setList()初始化了BoardList[],即把XML定义的9个带id信息的ImageList与BoardList关联起来。
findIndex(ImageView)方法用于得到ImageView的映射(0-8),具体是,左上的ImageView格子映射为0,中上的格子映射为1,...右下的格子映射为8。这样,每次点击其中一个格子,得到其映射i,就可以关联到BoardState[i/3][i%3],获取、修改该格子的状态,改变格子的图片代表落子。
private boolean hasWon() {
int[][] winningpattern = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, {0, 4, 8}, {2, 4, 6}};
StateEnum[] players = {StateEnum.GreenCross, StateEnum.RedCircle};
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 2; j++) {
StateEnum player = players[j];
if (BoardState[winningpattern[i][0] / 3][winningpattern[i][0] % 3] == player
&& BoardState[winningpattern[i][1] / 3][winningpattern[i][1] % 3] == player
&& BoardState[winningpattern[i][2] / 3][winningpattern[i][2] % 3] == player) {
return true;
}
}
}
return false;
}
hasWon()是判定BoardState的当前状态是否满足一方胜利的函数,若胜负已定,返回true。
winningpattern数组保存了井字棋胜利的所有连珠模式,如{0,1,2}代表0号(左上)、1号(中上)、2号(右上)三个格子内棋子持方一致。
如果还有什么不清楚的地方,或者其他想法与建议,欢迎留言私信~
The End