3.保龄球游戏

未匹配的标注
  • 本系列文章为laracasts.com 的系列视频教程——Code Katas in PHP 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频,支持正版
  • Kata 是一个简短,可重复的编程挑战,可以帮助我们进行快速地编程练习。
  • 开发模型仍旧是 TDD(测试驱动开发),视频中使用的是 phpspec 进行开发,笔记中使用了 Laravel 应用,因此代码有不同。

本节说明

  • 对应第 3 小节:Bowling Game

本节内容

这一节我们的练习是保龄球游戏。首先你需要花几分钟简单了解下 保龄球的规则,然后我们开始进行练习:

$ php artisan make:test BowlingGameTest --unit

然后新建第一个测试:20 轮空投则分数为 0,如下:

tests\Unit\BowlingGameTest.php

<?php

namespace Tests\Unit;

use Tests\TestCase;
use App\BowlingGame;

class BowlingGameTest extends TestCase
{
    public function setUp()
    {
        parent::setUp();

        $this->bowlingGame = new BowlingGame();
    }

    /** @test */
    public function it_scores_a_gutter_game_as_zero()
    {
        for($i=0;$i < 20;$i++)
        {
            $this->bowlingGame->roll(0);
        }

        $this->assertEquals(0,$this->bowlingGame->score());
    }
}

注:roll()函数的参数表示被击倒的瓶子数,如roll(0)表示击倒了 0 个瓶子。

然后运行测试:
file

注:我们在命令行使用alias pb="phpunit --filter BowlingGame"命令设置了别名

我们来让测试通过:

app\BowlingGame.php

<?php

namespace App;

class BowlingGame
{
    public function roll($roll)
    {

    }

    public function score()
    {
        return 0;
    }
}

再次测试:
file
现在可以新建第二个测试:统计所有被击倒的瓶子的分数。我们的测试思路是:如果每一次都只击倒了一个瓶子,那么会有 20 次roll(1),且总分为 20。如下:

    .
    .
    /** @test */
    public function it_scores_the_sum_0f_all_knocked_down_pins_for_a_game()
    {
        for($i=0;$i < 20;$i++)
        {
            $this->bowlingGame->roll(1);
        }

        $this->assertEquals(20,$this->bowlingGame->score());
    }
}

运行测试会失败:
file
我们需要修改我们的代码:

app\BowlingGame.php

<?php

namespace App;

class BowlingGame
{
    protected $score = 0;

    public function roll($roll)
    {
       $this->score += $roll; 
    }

    public function score()
    {
        return $this->score;
    }
}

我们简单地每次加上被击倒的瓶子数作为总得分,然后运行测试:
file
接下来我们要来测试 补中,即Spare。假设我们第一轮完成了Spare,然后剩下的掷球回合击倒数分别为 5 和 17 次 0 击倒,所以总分为:(10 + 5)+ 5 = 20。如下:

    .
    .
    /** @test */
    public function it_awards_a_roll_bonus_for_every_spare()
    {
        $this->bowlingGame->roll(2);
        $this->bowlingGame->roll(8); // get a spare

        $this->bowlingGame->roll(5);

        for($i=0;$i < 17;$i++)
        {
            $this->bowlingGame->roll(0);
        }

        $this->assertEquals(20,$this->bowlingGame->score());
    }
}

运行测试会失败:
file
现在我们需要修改代码了,然而,现在你会发现,实际上我们使用roll()方法来得到总分数,score()方法并没有起到计算总分的作用。我们应该用roll()来记录每一次掷球击倒瓶子的数量,而不是来计算得分。那么我们就需要对代码进行重构。当你要进行重构的时候,记住,你的测试必须全部通过。所以我们先注释掉新添加的测试,然后进行重构。

app\BowlingGame.php

<?php

namespace App;

class BowlingGame
{
    protected $rolls = [];

    public function roll($pints)
    {
       $this->rolls[] += $pints; 
    }

    public function score()
    {
        return array_sum($this->rolls);
    }
}

运行测试:
file
我们知道的是,一局游戏分为 10 轮,每轮 2 次掷球(通常状况下),所以接下来我们继续重构,引入轮次的概念:

<?php

namespace App;

class BowlingGame
{
    protected $rolls = [];

    public function roll($pints)
    {
       $this->rolls[] += $pints; 
    }

    public function score()
    {
        $score = 0;
        $roll = 0;

        for($frame = 1;$frame <= 10;$frame++)
        {
            $score += $this->rolls[$roll] + $this->rolls[$roll+1];

            $roll += 2;
        }
        return $score;
    }
}

每轮我们处理 2 次掷球的得分来计算得到总分数,现在我们来运行测试:
file
现在重构已经完成了,我们把注释掉的测试恢复,然后进行下一步地开发:

app\BowlingGame.php

    .
    .
    public function score()
    {
        $score = 0;
        $roll = 0;

        for($frame = 1;$frame <= 10;$frame++)
        {
            if($this->rolls[$roll] + $this->rolls[$roll+1] == 10){
                $score += 10;

                $score += $this->rolls[$roll+2];
            }else{
                $score += $this->rolls[$roll] + $this->rolls[$roll+1];
            }

            $roll += 2;
        }

        return $score;
    }
}

然后我们运行测试:
file
我们的测试通过了,但是我们仍然有工作要做,因为我们现在的代码的可读性很差,如果不借助注释,其他人很难明白代码的逻辑。所以我们需要进行些重构:

<?php

namespace App;

class BowlingGame
{
    protected $rolls = [];

    public function roll($pints)
    {
       $this->rolls[] += $pints; 
    }

    public function score()
    {
        $score = 0;
        $roll = 0;

        for($frame = 1;$frame <= 10;$frame++)
        {
            if($this->isSpare($roll)){
                $score += 10;

                $score += $this->rolls[$roll+2];
            }else{
                $score += $this->getDefaultFrameScore($roll);
            }

            $roll += 2;
        }

        return $score;
    }

    private function isSpare($roll)
    {
        return $this->rolls[$roll] + $this->rolls[$roll+1] == 10;
    }

    private function getDefaultFrameScore($roll)
    {
        return $this->rolls[$roll] + $this->rolls[$roll+1];
    }
}

对比$this->rolls[$roll] + $this->rolls[$roll+1] == 10$this->isSpare($roll)更能告诉阅读代码的人,所需的判断条件是什么。我们来运行测试:
file
一旦测试通过,我们就可以推进我们的开发工作。但是,我们之前也说过,测试通过也是我们重构代码的好时机。我们来对我们的测试进行些重构,以便更具可读性。我们至少有两个地方可以进行改善:

  1. 多次执行的for循环代码,我们可以封装到一个方法中;
  2. 获得一次spare的代码,我们需要做到不借助注释就能让人知道我们获得了一次spare

我们进行如下重构:
tests\Unit\BowlingGameTest.php

<?php

namespace Tests\Unit;

use App\BowlingGame;
use Tests\TestCase;

class BowlingGameTest extends TestCase
{
    public function setUp()
    {
        parent::setUp();

        $this->bowlingGame = new BowlingGame();
    }

    /** @test */
    public function it_scores_a_gutter_game_as_zero()
    {
        $this->rollTimes(20,0);

        $this->assertEquals(0,$this->bowlingGame->score());
    }

    /** @test */
    public function it_scores_the_sum_0f_all_knocked_down_pins_for_a_game()
    {
        $this->rollTimes(20,1);

        $this->assertEquals(20,$this->bowlingGame->score());
    }

    /** @test */
    public function it_awards_a_roll_bonus_for_every_spare()
    {
        $this->rollSpare();

        $this->bowlingGame->roll(5);

        $this->rollTimes(17,0);

        $this->assertEquals(20,$this->bowlingGame->score());
    }

    private function rollSpare()
    {
        $this->bowlingGame->roll(2);
        $this->bowlingGame->roll(8); 
    }

    private function rollTimes($times,$pints)
    {
        for($i=0;$i < $times;$i++)
        {
            $this->bowlingGame->roll($pints);
        }
    }
}

再次运行测试:
file
现在我们的代码变得很清晰简介,我们可以进行下一个测试:测试 全中,即Strike

    .
    .
    /** @test */
    public function it_awards_a__two_roll_bonus_for_every_strike()
    {
        $this->bowlingGame->roll(10);

        $this->bowlingGame->roll(7);
        $this->bowlingGame->roll(2);

        $this->rollTimes(17,0);

        $this->assertEquals(28,$this->bowlingGame->score());
    }

    private function rollSpare()
    {
        $this->bowlingGame->roll(2);
        $this->bowlingGame->roll(8); 
    }

    private function rollTimes($times,$pints)
    {
        for($i=0;$i < $times;$i++)
        {
            $this->bowlingGame->roll($pints);
        }
    }
}

我们第一次掷球获得了strike,那么该轮结束;第 2 轮我们两次掷球击倒的瓶子数分别为 7 和 2,那么我们的总分为:(10 + 7 + 2) +(7 + 2)= 28。现在我们运行测试会失败:
file

我们来让测试通过:

    .
    .
    public function score()
    {
        $score = 0;
        $roll = 0;

        for($frame = 1;$frame <= 10;$frame++)
        {
            if($this->rolls[$roll] == 10){
                $score += 10 + $this->rolls[$roll+1] + $this->rolls[$roll+2];
            }elseif($this->isSpare($roll)){
                $score += 10 + $this->rolls[$roll+2];
            }else{
                $score += $this->getDefaultFrameScore($roll);
            }

            $roll += 2;
        }
        .
        .

运行测试:
file
仍然失败,这是因为我们每轮都让roll += 2,即默认每轮都进行了 2 次掷球。但是对于strike,我们每轮只掷球一次。所以我们要进行修改:

    .
    .
    public function score()
    {
        $score = 0;
        $roll = 0;

        for($frame = 1;$frame <= 10;$frame++)
        {
            if($this->rolls[$roll] == 10){
                $score += 10 + $this->rolls[$roll+1] + $this->rolls[$roll+2];

                $roll += 1;
            }elseif($this->isSpare($roll)){
                $score += 10 + $this->rolls[$roll+2];

                $roll += 2;
            }else{
                $score += $this->getDefaultFrameScore($roll);

                $roll += 2;
            }
        }

        return $score;
    }
    .
    .

再次测试:
file
现在,正如之前我们所做的,重构代码使其更具可读性:

<?php

namespace App;

class BowlingGame
{
    protected $rolls = [];

    public function roll($pints)
    {
       $this->rolls[] += $pints; 
    }

    public function score()
    {
        $score = 0;
        $roll = 0;

        for($frame = 1;$frame <= 10;$frame++)
        {
            if($this->isStrike($roll)){
                $score += 10 + $this->getStrikeBonus($roll);

                $roll += 1;
            }elseif($this->isSpare($roll)){
                $score += 10 + $this->getSpareBonus($roll);

                $roll += 2;
            }else{
                $score += $this->getDefaultFrameScore($roll);

                $roll += 2;
            }
        }

        return $score;
    }

    private function isSpare($roll)
    {
        return $this->rolls[$roll] + $this->rolls[$roll+1] == 10;
    }

    private function isStrike($roll)
    {
       return $this->rolls[$roll] == 10;
    }

    private function getDefaultFrameScore($roll)
    {
        return $this->rolls[$roll] + $this->rolls[$roll+1];
    }

    private function getStrikeBonus($roll)
    {
        return $this->rolls[$roll+1] + $this->rolls[$roll+2];
    }

    private function getSpareBonus($roll)
    {
        return $this->rolls[$roll+2];
    }
}

然后运行测试:
file
最后让我们添加最后一个测试:满分局。这种情况的出现表明你连续获得了 10 次strike,所以最终会得到 300 分:

tests\Unit\BowlingGameTest.php

    .
    .
    /** @test */
    public function it_scores_a_perfect_game()
    {
        $this->rollTimes(12,10);

        $this->assertEquals(300,$this->bowlingGame->score());
    }
    .
    .

运行测试:
file
现在我们测练习暂时告一段落,但是也许你还想继续做些工作,例如:roll()的参数必须是正数之类的防护性工作,那就开始吧!

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~