三连棋游戏 Tic-tac-toe
两人轮流在印有九格方盘上划“X”或“O”字, 谁先把三个同一记号排成横线、直线、斜线, 即是胜者)。
以下是这个游戏的一个案例:
这个游戏的介绍可以参见:
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》一书。