diff --git a/README.md b/README.md index 02ed923..4ab5833 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Includes: - **Anagrams**: select an anagram for a word from a list of condidates. - **Bob**: returns different responses based on input. +- **Bowling**: calculate the score for a game of bowling. - **Grade School**: given students' names along with the grade that they are in, create a roster for the school. - **Roman numerals**: convert a number into its roman numeral value. diff --git a/src/BowlingGame.php b/src/BowlingGame.php new file mode 100644 index 0000000..807d33a --- /dev/null +++ b/src/BowlingGame.php @@ -0,0 +1,88 @@ +rolls = new Collection(); + } + + public function roll(int $pins): void + { + Assert::that($pins) + ->greaterOrEqualThan(0, + 'You cannot knock down a negative number of pins. Knocked down %d.') + ->lessOrEqualThan(self::MAX_PINS_PER_FRAME, + 'You can only knock down a maximum of 10 pins in a roll. Knocked down 12.'); + + $this->rolls->push($pins); + } + + public function getScore(): int + { + $score = 0; + $roll = 0; + + foreach (range(1, self::NUMBER_OF_FRAMES) as $frame) { + if ($this->isStrike($roll)) { + $score += $this->rolls[$roll]; + $score += $this->bonusForStrike($roll); + + $roll += 1; + continue; + } + + if ($this->isSpare($roll)) { + $score += $this->defaultFrameScore($roll); + $score += $this->bonusForSpare($roll); + + $roll += 2; + continue; + } + + $score += $this->defaultFrameScore($roll); + + $roll += 2; + } + + return $score; + } + + private function bonusForSpare(int $roll): int + { + return $this->rolls->get($roll + 2); + } + + private function bonusForStrike(int $roll): int + { + return $this->rolls[$roll + 1] + $this->rolls[$roll + 2]; + } + + private function defaultFrameScore(int $roll): int + { + return $this->rolls[$roll] + $this->rolls[$roll + 1]; + } + + private function isSpare(int $roll): bool + { + return $this->defaultFrameScore($roll) + == self::MAX_PINS_PER_FRAME; + } + + private function isStrike(int $roll): bool + { + return $this->rolls[$roll] == self::MAX_PINS_PER_FRAME; + } +} diff --git a/tests/BowlingGameTest.php b/tests/BowlingGameTest.php new file mode 100644 index 0000000..4563f95 --- /dev/null +++ b/tests/BowlingGameTest.php @@ -0,0 +1,134 @@ +game = new BowlingGame(); +}); + +it('counts an empty game', function (): void { + foreach (range(1, 20) as $roll) { + $this->game->roll(0); + } + + assertSame(0, $this->game->getScore()); +}); + +it('counts all single pins', function (): void { + foreach (range(1, 20) as $roll) { + $this->game->roll(1); + } + + assertSame(20, $this->game->getScore()); +}); + +it('adds a roll one bonus for a spare', function (): void { + $this->game->roll(5); + $this->game->roll(5); // Spare + + $this->game->roll(2); + + foreach (range(1, 17) as $roll) { + $this->game->roll(0); + } + + assertSame(14, $this->game->getScore()); +}); + +it('adds a two roll bonus for a strike', function (): void { + $this->game->roll(10); // Strike + + $this->game->roll(3); + $this->game->roll(6); + + foreach (range(1, 16) as $roll) { + $this->game->roll(0); + } + + assertSame(28, $this->game->getScore()); +}); + +it('adds an extra ball for a spare on the final frame', function (): void { + foreach (range(1, 18) as $roll) { + $this->game->roll(0); + } + + $this->game->roll(7); + $this->game->roll(3); // Spare + + $this->game->roll(5); + + assertSame(15, $this->game->getScore()); +}); + +it('adds an two extra balls for a strike on the final frame', function (): void { + foreach (range(1, 18) as $roll) { + $this->game->roll(0); + } + + $this->game->roll(10); // Strike + + $this->game->roll(4); + $this->game->roll(2); + + assertSame(16, $this->game->getScore()); +}); + +it('scores a perfect game', function (): void { + foreach (range(1, 12) as $roll) { + $this->game->roll(10); + } + + assertSame(300, $this->game->getScore()); +}); + +it('scores a normal game', function (): void { + $this->game->roll(4); + $this->game->roll(3); + + $this->game->roll(10); + + $this->game->roll(4); + $this->game->roll(1); + + $this->game->roll(0); + $this->game->roll(2); + + $this->game->roll(3); + $this->game->roll(7); + + $this->game->roll(8); + $this->game->roll(1); + + $this->game->roll(10); + + $this->game->roll(0); + $this->game->roll(7); + + $this->game->roll(1); + $this->game->roll(5); + + $this->game->roll(10); + $this->game->roll(6); + $this->game->roll(1); + + assertSame(103, $this->game->getScore()); +}); + +it('cannot knock down a negative pins', function (): void { + $this->game->roll(-1); +})->throws( + AssertionFailedException::class, + 'You cannot knock down a negative number of pins. Knocked down -1.' +); + +it('cannot knock down more than 10 pins in a roll', function (): void { + $this->game->roll(12); +})->throws( + AssertionFailedException::class, + 'You can only knock down a maximum of 10 pins in a roll. Knocked down 12.' +);