TDD案例-TicTacToe(井字棋游戏)

三连棋游戏 Tic-tac-toe

两人轮流在印有九格方盘上划“X”或“O”字, 谁先把三个同一记号排成横线、直线、斜线, 即是胜者)。
以下是这个游戏的一个案例:

image

这个游戏的介绍可以参见:
https://en.wikipedia.org/wiki/Tic-tac-toe

Tic-tac-toe的TDD过程

首先是棋盘

需求1:可将棋子放在3*3棋盘上任何没有棋子的地方

 * 定义边界,以及将棋子放在哪些地方非法。可以有如下的三个测试
 * 1)超出X轴边界
 * 2)超出Y轴边界
 * 3)落子的地方已经有棋子

我们可以编写如下的测试用例

package com.github.tdd.tictactoe;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class TestTictactoe {
    Tictactoe ticTactoe = new Tictactoe();

    @Test
    public void WhenXOutOfBoardThenThrowException(){
        assertThatThrownBy(() -> ticTactoe.play(5,2))
                .isInstanceOf(RuntimeException.class)
                .hasMessage("X is out of board");
    }
    @Test
    public void WhenYOutOfBoardThenThrowException(){
        assertThatThrownBy(() -> ticTactoe.play(2,5))
                .isInstanceOf(RuntimeException.class)
                .hasMessage("Y is out of board");
    }
    @Test
    public void WhenOccupiedThenThrowException(){
        ticTactoe.play(2,2);
        assertThatThrownBy(() -> ticTactoe.play(2,2))
                .isInstanceOf(RuntimeException.class)
                .hasMessage("Occupied");
    }

然后是根据测试用例,在Tictactoe类中实现play方法,让这三个用例通过。

package com.github.tdd.tictactoe;

public class Tictactoe {
    private Character [][] board ={{'\0','\0','\0'},
                                       {'\0','\0','\0'},
                                       {'\0','\0','\0'}};
    public void play(int x, int y) {
        if(x<1 || x>3) {
            throw new RuntimeException("X is out of board");
        }else if(y<1 || y>3) {
            throw new RuntimeException("Y is out of board");
        }
        if(board[x-1][y-1]!='\0'){
            throw new RuntimeException("Occupied");
        } else {
            board[x-1][y-1]='X';
        }
    }
}

按照TDD的思路,play的实现只要满足测试用例通过就行。

需求2:需要提供一种途径,用于判断接下来该谁落子

 * 现在处理轮到哪个玩家落子的问题。也可以有如下三个测试:
 * 1)玩家X先下
 * 2)如果上一次是X下的,接下来将轮到O下;
 * 3)如果上一次是O下的,接下来将轮到X下。

于是,我们再写3个测试用例。

    @Test
    public void TestXPlayFirst(){
        assertThat(ticTactoe.nextPlayer()).isEqualTo('X');
    }
    @Test
    public void TestGivenLastTurnXNextPlayerIsO(){
        ticTactoe.play(1,1);
        assertThat(ticTactoe.nextPlayer()).isEqualTo('O');
    }

为了能让上述用例通过,我们需要在Tictactoe 类中引入nextPlayer方法,并记录下当前玩家。

    private char lastPlayer='\0';
    public char nextPlayer() {
        if (lastPlayer=='X'){
        return 'O';
        }
        return 'X';
    }

根据需求,游戏首先是由X先下,然后是O交替下。同时,我们可以在实现新需求的同时,对原先检查是否超出棋盘的代码进行重构优化。现在的Tictactoe实现如下:

package com.github.tdd.tictactoe;

public class Tictactoe {
    private Character [][] board ={{'\0','\0','\0'},{'\0','\0','\0'},{'\0','\0','\0'}};
    private char lastPlayer='\0';
    public static final String NOWINNER="No Winner";
    public static final String XWINNER="X is Winner";
    public String play(int x, int y) {
        checkX(x);
        checkY(y);
        lastPlayer=nextPlayer();
        setBox(x,y,lastPlayer);
    }
    private void checkX(int x){
        if(x<1 || x>3) {
            throw new RuntimeException("X is out of board");
        }
    }
    private void checkY(int y){
        if(y<1 || y>3) {
            throw new RuntimeException("Y is out of board");
        }
    }
    private void setBox(int x,int y,char lastPlayer){
        if(board[x-1][y-1]!='\0'){
            throw new RuntimeException("Occupied");
        } else {
            board[x-1][y-1]=lastPlayer;
        }
    }
    public char nextPlayer() {
        if (lastPlayer=='X'){
        return 'O';
        }
        return 'X';
    }
}

需求3:获胜规则,最先在水平、垂直或对角线上将自己的3个标记连起来的玩家获胜

在实现了棋盘、下法之后,现在可以来实现获胜规则了。
* 检查是否获胜的用例有
* 1)如果不满足获胜条件,则无人获胜
* 2)一个玩家的棋子占据整条水平线就赢了
* 3)一个玩家的棋子占据整条垂直线就赢了
* 4)一个玩家的棋子占据从左上到右下角的整条对角线就赢了
* 5)一个玩家的棋子占据从左下到右上角的整条对角线就赢了

    @Test
    public void TestNoWinnerYet(){      
assertThat(ticTactoe.play(1,1)).isEqualTo(Tictactoe.NOWINNER);
    }
    @Test
    public void TestWinWhenTheWholeHorizontalLine(){
        ticTactoe.play(1,1); //X
        ticTactoe.play(1,2); //O
        ticTactoe.play(2,1); //X
        ticTactoe.play(2,2); //O
        assertThat(ticTactoe.play(3,1)).isEqualTo(Tictactoe.XWINNER); //X
    }

    @Test
    public void TestWinWhenTheWholeVerticalLine(){
        ticTactoe.play(1,1); //X
        ticTactoe.play(2,1); //O
        ticTactoe.play(1,2); //X
        ticTactoe.play(2,2); //O
        ssertThat(ticTactoe.play(1,3)).isEqualTo(Tictactoe.XWINNER); //X
    }
    //
    @Test
    public void TestWinWhenBottomToTopDiagonalLine(){
        ticTactoe.play(1,3); //X
        ticTactoe.play(2,1); //O
        ticTactoe.play(2,2); //X
        ticTactoe.play(2,3); //O
        assertThat(ticTactoe.play(3,1)).isEqualTo(Tictactoe.XWINNER); //X
    }

这里就需要在play方法中增加对于是否有人获胜的判断逻辑 。根据上述用例,可以写出如下的 isWin ()代码

        private boolean isWin () {
            int total = lastPlayer * SIZE;
            char diagonal1 = '\0';
            char diagonal2 = '\0';
            for (int index = 0; index < SIZE; index++) {
                if (board[0][index] + board[1][index] + board[2][index] 
== total) {
                    return true;   //行
                } else 
if (board[index][0] + board[index][1] + board[index][2] == total) {
                    return true; //列
                }
                diagonal1 += board[index][index];
                diagonal2 += board[index][SIZE - index - 1];
            }
//        if (board[0][0] + board[1][1]  + board[2][2]  == total) {
//            return true; //对角
//        }
//        if (board[0][2] + board[1][1]  + board[2][0]  == total) {
//            return true; //对角
//        }
            return diagonal1 == total || diagonal2 == total;
        }

需求4:处理平局,所有格子都占满则为平局

还是先写用例

    @Test
    public void TestDrawWhenAllBoxesOccupied(){
        ticTactoe.play(1,1); //X
        ticTactoe.play(1,2); //O
        ticTactoe.play(1,3); //X
        ticTactoe.play(2,1); //O
        ticTactoe.play(2,3); //X
        ticTactoe.play(2,2); //O
        ticTactoe.play(3,1); //X
        ticTactoe.play(3,3); //O
        assertThat(ticTactoe.play(3,2)).isEqualTo("DRAW"); //X
    }
}

然后在play方法中增加isDraw()判断来让上述用例通过。

   private boolean isDraw() {
        for (int index = 0; index < SIZE; index++) {
            for (int j = 0; j < SIZE; j++) {
                if (board[index][j] == '\0') {
                    return false;
                }
            }
        }
            return true;
    }

最终的TicTacToe类是这样的

package com.github.tdd.tictactoe;

public class Tictactoe {
    private Character[][] board = {{'\0', '\0', '\0'}, 
{'\0', '\0', '\0'}, {'\0', '\0', '\0'}};
    private char lastPlayer = '\0';
    private int SIZE = 3;
    public static final String NOWINNER = "No Winner";
    public static final String XWINNER = "X is Winner";

    public String play(int x, int y) {
        checkX(x);
        checkY(y);
        lastPlayer = nextPlayer();
        setBox(x, y, lastPlayer);
        if (isWin()) {
            return lastPlayer + " is Winner";
        } else if (isDraw()) {
            return "DRAW";
        } else {
            return NOWINNER;
        }
    }

    private void checkX(int x) {
        if (x < 1 || x > 3) {
            throw new RuntimeException("X is out of board");
        }
    }

    private void checkY(int y) {
        if (y < 1 || y > 3) {
            throw new RuntimeException("Y is out of board");
        }
    }

    private void setBox(int x, int y, char lastPlayer) {
        if (board[x - 1][y - 1] != '\0') {
            throw new RuntimeException("Occupied");
        } else {
            board[x - 1][y - 1] = lastPlayer;
        }
    }

    private boolean isDraw() {
        for (int index = 0; index < SIZE; index++) {
            for (int j = 0; j < SIZE; j++) {
                if (board[index][j] == '\0') {
                    return false;
                }
            }
        }
            return true;
    }
        private boolean isWin () {
            int total = lastPlayer * SIZE;
            char diagonal1 = '\0';
            char diagonal2 = '\0';
            for (int index = 0; index < SIZE; index++) {
                if (board[0][index] + board[1][index] + board[2][index] 
== total) {
                    return true;
                } 
else if (board[index][0] + board[index][1] + board[index][2] == total) {
                    return true;
                }
                diagonal1 += board[index][index];
                diagonal2 += board[index][SIZE - index - 1];
            }
            return diagonal1 == total || diagonal2 == total;
        }

        public char nextPlayer () {
            if (lastPlayer == 'X') {
                return 'O';
            }
            return 'X';
        }

}

代码覆盖率

根据上述4个需求,经过TDD以后得到的TicTacToe实现。现在我们来关注下代码覆盖率。


TDD案例-TicTacToe(井字棋游戏)_第1张图片
image.png

100%的行覆盖。
该案例来自 《Test-Driven Java Development》一书。

你可能感兴趣的:(TDD案例-TicTacToe(井字棋游戏))