首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TDD案例-三连棋游戏 Tic-tac-toe

TDD案例-三连棋游戏 Tic-tac-toe

作者头像
Antony
发布2021-09-02 15:39:52
1.1K0
发布2021-09-02 15:39:52
举报

三连棋游戏 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实现。现在我们来关注下代码覆盖率。

100%的行覆盖。

该案例来自 《Test-Driven Java Development》一书。

原稿写于2020年初疫情期间,最近给单位新人培训用上了,故重发。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 软件测试那些事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 三连棋游戏 Tic-tac-toe
  • Tic-tac-toe的TDD过程
    • 需求1:可将棋子放在3*3棋盘上任何没有棋子的地方
      • 需求2:需要提供一种途径,用于判断接下来该谁落子
        • 需求3:获胜规则,最先在水平、垂直或对角线上将自己的3个标记连起来的玩家获胜
          • 需求4:处理平局,所有格子都占满则为平局
            • 代码覆盖率
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档