Android:實現井字棋小遊戲

 

井字棋實現方式有許多,最簡單的方法是將9個imagView組成棋盤,然後通過一些邏輯設計進行遊戲。本文采用的是自定義View的方式進行遊戲設計,通過繼承View進行棋盤,選中狀態繪製,效果如下:

 

1.棋盤繪製

  通過選取設置寬高中最小值作爲控件寬高,並平均分爲3段作爲每小格的長度,在onDraw()方法中繪製棋盤,代碼如下:

//重寫onMeasure()設置寬高
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int size = Math.min(MeasureSpec.getSize(widthMeasureSpec),MeasureSpec.getSize(heightMeasureSpec));
        setMeasuredDimension(size,size);
    }


//棋盤畫筆
private Paint mPaint;

@Override
    protected void onDraw(Canvas canvas) {
        //每小格長度
        length = getWidth()/3;
        //棋盤繪製
        for(int i = 0;i < 4;i++){
            canvas.drawLine(length*i,0,length*i,3*length,mPaint);
            canvas.drawLine(0,length*i,length*3,length*i,mPaint);
        }
}

 

 2.選中狀態繪製

  爲了區分選中操作的玩家,新建枚舉類型:

//代表不同玩家(NONE用來表示平局情況獲勝玩家)
    public enum Player{
        USER_ONE,USER_TWO,NONE
    }

 當玩家點擊屏幕時,我們需要知道他所選的格子是哪一個,通過實現 View.OnTouchListener 接口進行監聽,代碼如下(其中的判斷是避免滑動情況):

 // down 事件 座標
    private float lastX;
    private float lastY;

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_UP:
                // UP 事件座標 與 Down事件座標 距離不能太遠
                if(Math.abs(lastX-x)<length/2 && Math.abs(lastY-y)<length/2){
                    //通過UP事件座標計算選中格子
                    calculateTouchItem(x,y);
                }
                break;
        }
        return true;
    }

 其中calculateTouchItem方法功能爲計算選中格子,代碼如下:

//被選格子(玩家選擇前先通過此數組判斷是否可選,選擇後將對應格子值設爲非0)
private int[] locations = new int[9];
//每個格子對應字母編號,將通過字母組合判斷是否獲勝
private String[] items = new String[9];
//玩家所選格子對應字母集合
private List<String> user1Selected = new ArrayList<>();
private List<String> user2Selected = new ArrayList<>();
//已選格子信息集合(格子所選玩家,格子中心點座標)
private List<ItemInformation> mList = new ArrayList<>();

//當前回合玩家
private Player currentPlayer;

private void calculateTouchItem(float x, float y) {
        //判斷所在行列
        int j = (int)x/length;
        int i = (int)y/length;
        //判斷是否在棋盤內
        if(i < 3 && j < 3){
            //判斷是否可選
            if(locations[i*3+j] == 0){
                //創建選中格子信息並添加到選中格子list中
                ItemInformation itemInformation = new ItemInformation(items[i*3+j],currentPlayer,i,j);
                mList.add(itemInformation);
                //將格子設爲已選狀態
                locations[i*3+j] = 1;
                //將選中格子字母編號添加到當前玩家選中格子list中
                if(currentPlayer == Player.USER_ONE){
                    user1Selected.add(items[i*3+j]);
                }else {
                    user2Selected.add(items[i*3+j]);
                }
                //重繪
                invalidate();
            }
        }

    }

其中ItemInformation保存選中格子相關信息,具體如下:

//存儲選中格子信息
    private class ItemInformation{

        private int i;
        private int j;
        //選中格子字母編號
        private String location;
        private Player player;

        public ItemInformation(String location, Player player,int i,int j) {
            this.location = location;
            this.player = player;
            this.i = i;
            this.j = j;
        }

        public String getLocation() {
            return location;
        }

        public Player getPlayer() {
            return player;
        }

        public int getI() {
            return i;
        }

        public int getJ() {
            return j;
        }
    }

 九個格子字母編號和數字編號如下:

 A(0) B(1) C(2)
D(3) E(4) F(5)
G(6) H(7) I(8)

計算好選中格子後調用invalidate()方法進行重繪,修改onDraw()方法,代碼如下:

//是否第一次加載
private boolean isFirst = true;
 //玩家圖案對應顏色
private int userColorOne;
private int userColorTwo;

@Override
    protected void onDraw(Canvas canvas) {
        length = Math.min(getWidth(),getHeight())/3;
        //棋盤繪製
        for(int i = 0;i < 4;i++){
            canvas.drawLine(length*i,0,length*i,3*length,mPaint);
            canvas.drawLine(0,length*i,length*3,length*i,mPaint);
        }
        //選中格子繪製
        if(!isFirst){
            for(int i = 0;i < mList.size();i++){
                switch (mList.get(i).getPlayer()){
                    case USER_ONE:
                        userPaint.setColor(userColorOne);
                        drawUserSelected(canvas,mList.get(i));
                        break;
                    case USER_TWO:
                        userPaint.setColor(userColorTwo);
                        drawUserSelected(canvas,mList.get(i));
                        break;
                }
            }
            //查看遊戲狀態是否結束
            checkStatus();

        }
        if(isFirst){
            isFirst = false;
        }
    }

 遍歷所有選中格子,通過對應ItemInformation信息知道所選玩家,設置對應顏色,再調用drawUserSelected()方法繪製,具體代碼如下:

//玩家選擇圖案畫筆
private Paint userPaint; 

//繪製選中格子
    private void drawUserSelected(Canvas canvas, ItemInformation itemInformation) {
        //計算格子中心座標
        int centerX = itemInformation.getJ() * length + length/2;
        int centerY = itemInformation.getI() * length + length/2;
        userPaint.setStrokeWidth(5f);
        //玩家一 繪製 × 圖案
        if(itemInformation.getPlayer() == Player.USER_ONE){
            float delta = (float) Math.sqrt(0.08*length*length);
            canvas.drawLine(centerX-delta,centerY-delta,centerX+delta,centerY+delta,userPaint);
            canvas.drawLine(centerX+delta,centerY-delta,centerX-delta,centerY+delta,userPaint);
        }else {
            //玩家二 繪製 ○ 圖案
            float radius =  0.4f * length;
            canvas.drawCircle(centerX,centerY,radius,userPaint);
        }
    }

 繪製後就調用checkStatus()方法進行判斷,是否有玩家獲勝,是否還有未選格子,如遊戲還能繼續則切換玩家。具體代碼如下:

//查看遊戲狀態
    private void checkStatus(){
        // 避免非用戶點擊情況下 onDraw()方法調用 造成 當前玩家的切換
        if(mList.size() == 0 || mList.size()==lastCount) return;
        lastCount = mList.size();
        //查看是否有人獲勝
        boolean isSuccess = checkIsSuccessful();
        if(isSuccess){
            if(mListener != null){
                mListener.onSuccess(currentPlayer);
            }
            setOnTouchListener(null);
        }else {
            //判斷是否平局
            if(mList.size() == 9){
                if(mListener != null){
                    mListener.onSuccess(Player.NONE);
                }
                return;
            }
            //切換當前用戶
            switch (currentPlayer){
                case USER_TWO:
                    currentPlayer = Player.USER_ONE;
                    break;
                case USER_ONE:
                    currentPlayer = Player.USER_TWO;
                    break;
            }
        }
        Log.e("Chess:",currentPlayer+"");
    }

  其中checkIsSuccessful()方法判斷有人獲勝,具體代碼如下:

private boolean checkIsSuccessful() {
        boolean isSuccess = false;
        String tmp = "";
        if(currentPlayer == Player.USER_ONE){
            if(user1Selected.size() >= 3){
                //將玩家所選格子字母編號排序
                Collections.sort(user1Selected);
                //回溯法判斷是否獲勝
                searchResult(user1Selected,tmp,0);
                isSuccess = result.size() > 0;
            }
        }else {
            if(user2Selected.size() >= 3){
                Collections.sort(user2Selected);
                searchResult(user2Selected,tmp,0);
                isSuccess = result.size() > 0;
            }
        }
        return isSuccess;
    }

 通過調用searchResult()方法進行結果查找,具體代碼如下:

//獲勝的所有格子字母組合(按字典序排序)
private List<String> successResult = new ArrayList<>();
//存儲玩家獲勝的字母組合
private List<String> result = new ArrayList<>();


 //回溯法 將所有情況進行判斷
    private void searchResult(List<String> userSelected,String tmp,int index) {
        if(tmp.length() == 3){
            System.out.println(tmp);
            if(successResult.contains(tmp)){
                result.add(tmp);
            }
            return;
        }
        for(int i = index;i < userSelected.size();i++){
            tmp += userSelected.get(i);
            searchResult(userSelected,tmp,i+1);
            tmp = tmp.substring(0,tmp.length()-1);
        }

    }

 其中successResult存儲了所有獲勝情況下的字母組合(按字典序),在checkIsSuccessful()方法中先調用Collections.sort()將玩家所選格子字母編號進行排序,在通過回溯法將所有3個字母組合與successResult結果進行比較,如果存在即獲勝,將結果加入result中,在checkIsSuccessful()方法中可通過result長度可知是否獲勝。如果有人獲勝則回調遊戲結束接口,接口如下:

//遊戲結束監聽器
    private OnSuccessListener mListener;

//遊戲結束回調接口
    public interface OnSuccessListener{
        public void onSuccess(Player player);
    }

//設置遊戲結束回調接口
    public void setOnSuccessListener(OnSuccessListener listener){
        mListener = listener;
    }

  回調代碼在checkStatus()方法,如下:

 //查看遊戲狀態
    private void checkStatus(){
        // 避免非用戶點擊情況下 onDraw()方法調用 造成 當前玩家的切換
        if(mList.size() == 0 || mList.size()==lastCount) return;
        lastCount = mList.size();
        //查看是否有人獲勝
        boolean isSuccess = checkIsSuccessful();
        if(isSuccess){
            if(mListener != null){
                mListener.onSuccess(currentPlayer);
            }
            setOnTouchListener(null);
        }else {
            //判斷是否平局
            if(mList.size() == 9){
                if(mListener != null){
                    mListener.onSuccess(Player.NONE);
                }
                return;
            }
            //切換當前用戶
            switch (currentPlayer){
                case USER_TWO:
                    currentPlayer = Player.USER_ONE;
                    break;
                case USER_ONE:
                    currentPlayer = Player.USER_TWO;
                    break;
            }
        }
        Log.e("Chess:",currentPlayer+"");
    }

到此我們的自定義view也完成了,上面代碼中的變量在構造函數中,進行初始化,其中部分屬性可設爲自定義屬性供用戶設置,自定義屬性及變量初始化如下:

<declare-styleable name="ChessView">
        <attr name="lineColor" format="color"/>
        <attr name="user_color_one" format="color"/>
        <attr name="user_color_two" format="color"/>
    </declare-styleable>
    public ChessView(Context context) {
        this(context,null);
    }

    public ChessView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public ChessView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context,attrs);
    }


    private void init(Context context,AttributeSet attributeSet) {
        initData();
        TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.ChessView);
        lineColor = typedArray.getColor(R.styleable.ChessView_lineColor,Color.BLACK);
        userColorOne = typedArray.getColor(R.styleable.ChessView_user_color_one,Color.BLACK);
        userColorTwo = typedArray.getColor(R.styleable.ChessView_user_color_two,Color.RED);
        typedArray.recycle();

        userPaint = new Paint();
        userPaint.setStyle(Paint.Style.STROKE);
        userPaint.setAntiAlias(true);

        mPaint = new Paint();
        mPaint.setColor(lineColor);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);

        currentPlayer = Player.USER_ONE;
    }

    private void initData(){
        items[0] = "A";
        items[1] = "B";
        items[2] = "C";
        items[3] = "D";
        items[4] = "E";
        items[5] = "F";
        items[6] = "G";
        items[7] = "H";
        items[8] = "I";
        successResult.add("ABC");
        successResult.add("DEF");
        successResult.add("GHI");
        successResult.add("ADG");
        successResult.add("BEH");
        successResult.add("CFI");
        successResult.add("AEI");
        successResult.add("CEG");
    }

  我們可在佈局中使用此自定義view並實現遊戲結束接口,具體就不介紹了,代碼都有註釋,佈局代碼和activity代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".ChessActivity">

    <com.example.chess.ChessView
        android:background="#ffffff"
        android:id="@+id/chess_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/chess_button"
        android:textSize="22sp"
        android:textColor="#000000"
        android:background="#03A9F4"
        android:layout_marginTop="20dp"
        app:layout_constraintTop_toBottomOf="@id/chess_view"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_width="250dp"
        android:layout_height="70dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>
public class ChessActivity extends AppCompatActivity implements View.OnClickListener {

    private ChessView chessView;
    private Button chessButton;

    //遊戲結束信息提醒
    private AlertDialog.Builder alertDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chess);
        initDialog();
        //判斷是否爲第一次進入遊戲 是的話需點擊開始遊戲,否則直接進行遊戲
        Intent intent = getIntent();
        chessButton = findViewById(R.id.chess_button);
        chessView = findViewById(R.id.chess_view);

        if(intent.getIntExtra("re",0) == 1){
            chessButton.setVisibility(View.INVISIBLE);
            //使棋盤接收點擊事件
            chessView.setOnTouchListener();
        }else {
            chessButton.setText("開始遊戲");
            chessButton.setOnClickListener(this);
        }


        chessView.setOnSuccessListener(new ChessView.OnSuccessListener() {
            @Override
            public void onSuccess(ChessView.Player player) {
                //根據回調結果顯示結束信息
                switch (player){
                    case NONE:
                        alertDialog.setMessage("平局!");  //設置提示信息
                        break;
                    case USER_ONE:
                        alertDialog.setMessage("×方獲勝!");  //設置提示信息
                        break;
                    case USER_TWO:
                        alertDialog.setMessage("○方獲勝!");  //設置提示信息
                        break;
                }
                alertDialog.show(); //顯示
            }
        });
    }

    //初始化Dialog
    private void initDialog(){
        alertDialog = new AlertDialog.Builder(this);
        alertDialog.setTitle("遊戲信息");  //設置標題
        alertDialog.setCancelable(false);   //是否點擊屏幕可取消
        //確定按鈕點擊事件
        alertDialog.setPositiveButton("再來一局", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Intent intent = new Intent(ChessActivity.this,ChessActivity.class);
                intent.putExtra("re",1);
                startActivity(intent);
                finish();
            }
        });
        //取消按鈕點擊事件
        alertDialog.setNegativeButton("返回", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                chessButton.setVisibility(View.VISIBLE);
                chessButton.setText("再來一局");
                chessButton.setOnClickListener(ChessActivity.this);
            }
        });
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.chess_button) {
            if (chessButton.getText().toString().equals("開始遊戲")) {
                Log.e("ChessActivity:","開始遊戲");
                chessView.setOnTouchListener();
            } else {
                Intent intent = new Intent(ChessActivity.this, ChessActivity.class);
                intent.putExtra("re", 1);
                startActivity(intent);
                finish();
            }
        }
    }
}

 代碼已上傳GitHub,地址 : https://github.com/YangRT/Chess

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章