专栏首页软件测试那些事TDD案例-三连棋游戏 Tic-tac-toe

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

三连棋游戏 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 删除。
登录 后参与评论
0 条评论

相关文章

  • 在家隔离,不忘学习-三连棋游戏 Tic-tac-toe

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

    Antony
  • python实现简单井字棋游戏

    井字棋,英文名叫Tic-Tac-Toe,是一种在3*3格子上进行的连珠游戏,和五子棋类似,由于棋盘一般不画边框,格线排成井字故得名。游戏需要的工具仅为纸和笔,然...

    砸漏
  • LeetCode 348. 判定井字棋胜负(计数)

    请在 n × n 的棋盘上,实现一个判定井字棋(Tic-Tac-Toe)胜负的神器,判断每一次玩家落子后,是否有胜出的玩家。

    Michael阿明
  • python实现简单井字棋小游戏

    砸漏
  • 对称、群论与魔术(八)——魔术《tic tac toe》中的数学奇迹

    当时还剩下最后一个问题,那就是,我们的策略一定能够得到平局结果吗?如果我们还想要得到C4范围内的棋局结果,还需要做哪些策略定制呢?

    magic2728
  • 重磅 | 经典教材 R. Sutton《增强学习导论》最新版(451PDF)

    2016年10月18日, 世界人工智能大会技术分论坛,特设“新智元智库院长圆桌会议”,重量级研究院院长 7 剑下天山,汇集了中国人工智能产学研三界最豪华院长阵容...

    新智元
  • 8年打磨,《游戏设计梦工厂》发布史诗级更新!

    ?点击“博文视点Broadview”,获取更多书讯 从新石器时代的陶陀螺; 到古埃及的夹棋(Seega); 从象棋到围棋; 从桌游到剧本杀; 从俄罗斯方块、...

    博文视点Broadview
  • 重磅 | 经典教材 R. Sutton《增强学习导论》最新版(548PDF)

    新智元
  • 我用千行代码做了python版AI五子棋?还真玩不过AI了!!

    机器博弈是人工智能领域的重要分支,它的研究对象多以复杂的棋牌类智力游戏为主,已经得到解决的棋类游戏,几乎全部都应归功于机器博弈近半个世纪的发展。计算机解决问题的...

    川川菜鸟
  • 对称、群论与魔术(七)——魔术《tic tac toe》的奇迹&Tally-Ho牌背秘密公开!

    先信守承诺,公布一下Tally-Ho牌背图案的非对称性秘密,我大胆揣测这一定是有意为之。因为如果想做到对称,像素级别的成立,对于这种级别的印刷厂,和这个规模的印...

    magic2728
  • 五子棋人机对战思路「建议收藏」

    五子棋人机对战: 人机对战,我们可以想象一下我们在玩QQ游戏五子棋时的场景,根据每次下的步骤来分析电脑是怎样解析我们下棋的步骤的。

    全栈程序员站长
  • OpenAI Gym 入门

    这一部分参考官网提供的文档[1],对 Gym 的运作方式进行简单的介绍。Gym 是一个用于开发和比较强化学习算法的工具包,其对「代理」(agent)的结构不作要...

    口仆
  • 游戏人工智能 读书笔记 (二) 游戏人工智能简史

    关于作者:Fled在新加坡国立大学获得博士学位,现就职于腾讯游戏AI研究中心。 本文内容包含以下章节: Chapter 1.2 A Brief History...

    腾讯高校合作
  • 游戏人工智能 读书笔记 (二) 游戏人工智能简史

    人工智能发展的早期,绝大部分游戏AI的研究者都在努力做出一个很牛的AI在棋类游戏中战胜人类。这里面一部分的原因是,棋类游戏蕴含着一些人类智能的基本因素。

    WeTest质量开放平台团队
  • 游戏人工智能 读书笔记 (二) 游戏人工智能简史

    Chapter 1.2 A Brief History of Artificial Intelligence and Games

    鹅厂优文
  • Richard S. Sutton经典图书:《强化学习导论》第二版(附PDF下载)

    【导读】Richard S. Sutton就职于iCORE大学计算机科学系,是强化学习领域的专家,其在强化学习领域的著作“Reinforcement Learn...

    WZEARW
  • 也说棋类游戏

    之前自己编写过一点关于棋类游戏的代码,所以对于这类游戏的大致构成也算是有一些肤浅的认识,前一阵子突然想到应该将这些个零散知识好好总结一番,以算作为自己学习的一...

    用户2615200
  • 一个完整的TDD演练案例(四)

    说明:本讲义是我在ThoughtWorks作为咨询师时,为客户开展TDD Code Kata而编写。案例为Guess Number,案例需求来自当时的同事王瑜珩...

    张逸
  • 科普 | 划分人工智能水平的四个等级

    人工智能(Artificial Intelligence,简称AI),是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。既...

    达观数据

扫码关注腾讯云开发者

领取腾讯云代金券