-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[WIP] Implement exercise bowling #790
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
c382caa
Implement exercise bowling
thecouchcoder ad21e2a
bowling: created the files
thecouchcoder 1e16f9e
bowling: implemented test for an all 0 game
thecouchcoder 5b29088
bowling: passing all zeros test
thecouchcoder 028ce96
bowling tested and implemented logic for spares
thecouchcoder 5b61ca4
bowling: bonus roll implemented
thecouchcoder 4527240
bowling: heavily refactored code. Now passing 9 tests
thecouchcoder 1785b1f
bowling: tested and implemeneted first 14 tests
thecouchcoder 73c311f
fix travis issues with line length
thecouchcoder 3eb75d1
bowling: setup config
thecouchcoder 9fb30a7
bowling: passing 19 tests
thecouchcoder 090300d
bowling: fixed error in config.json
thecouchcoder ff70fd5
bowling: heavily refactoring code to implement frame logic during rol…
thecouchcoder 293bf44
bowling: still alot of refactoring going on and alot is broken
thecouchcoder 0cd0fc1
bowling: still refactoring and still alot of errors, but closer
thecouchcoder 3f1777e
bowling: nearly done refactoring
thecouchcoder 55c3c6c
bowling: finally back to passing all tests like before refactor
thecouchcoder 53b6e52
bowling: added a few more tests and cleaned up after refactor
thecouchcoder be6cf1c
bowling: passes all tests
thecouchcoder 91d1cf0
bowling: fixing travis issue
thecouchcoder 6034a95
bowling: fixes as requested
thecouchcoder 511732a
bowling: fixes as requested
thecouchcoder 5298cbb
bowling: Travis fixes
thecouchcoder d3c349d
bowling: Travis fixes
thecouchcoder 833dce2
bowling: Travis fixes
thecouchcoder 1cf050f
bowling: Travis fixes
thecouchcoder 5f8b980
bowling: fixes as requested
thecouchcoder dca0f0e
Merge branch 'master' into bowling
cmccandless 95b7666
Merge branch 'master' into bowling
cmccandless de5a2bb
Merge branch 'master' into bowling
cmccandless File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
|
||
|
||
class BowlingGame(object): | ||
def __init__(self): | ||
pass | ||
|
||
def roll(self, pins): | ||
pass | ||
|
||
def score(self): | ||
pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
import unittest | ||
|
||
from bowling import BowlingGame | ||
|
||
|
||
# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.1 | ||
|
||
class BowlingTests(unittest.TestCase): | ||
def setUp(self): | ||
self.game = BowlingGame() | ||
|
||
def roll(self, rolls): | ||
[self.game.roll(roll) for roll in rolls] | ||
|
||
def roll_and_score(self, rolls): | ||
self.roll(rolls) | ||
return self.game.score() | ||
|
||
def test_should_be_able_to_score_a_game_with_all_zeros(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 0) | ||
|
||
def test_should_be_able_to_score_a_game_with_no_strikes_or_spares(self): | ||
rolls = [3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 90) | ||
|
||
def test_a_spare_follow_by_zeros_is_worth_ten_points(self): | ||
rolls = [6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 10) | ||
|
||
def test_points_scored_in_the_roll_after_a_spare_are_counted_twice(self): | ||
rolls = [6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 16) | ||
|
||
def test_consecutive_spares_each_get_a_one_roll_bonus(self): | ||
rolls = [5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 31) | ||
|
||
def test_last_frame_spare_gets_bonus_roll_that_is_counted_twice(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 17) | ||
|
||
def test_a_strike_earns_ten_points_in_a_frame_with_a_single_roll(self): | ||
rolls = [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 10) | ||
|
||
def test_two_rolls_points_after_strike_are_counted_twice(self): | ||
rolls = [10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 26) | ||
|
||
def test_consecutive_stikes_each_get_the_two_roll_bonus(self): | ||
rolls = [10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 81) | ||
|
||
def test_strike_in_last_frame_gets_two_roll_bonus_counted_once(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
10, 7, 1] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 18) | ||
|
||
def test_rolling_spare_with_bonus_roll_does_not_get_bonus(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | ||
0, 10, 7, 3] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 20) | ||
|
||
def test_strikes_with_the_two_bonus_rolls_do_not_get_bonus_rolls(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, | ||
10, 10] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 30) | ||
|
||
def test_strike_with_bonus_after_spare_in_last_frame_gets_no_bonus(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, | ||
3, 10] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 20) | ||
|
||
def test_all_strikes_is_a_perfect_game(self): | ||
rolls = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 300) | ||
|
||
def test_rolls_cannot_score_negative_points(self): | ||
|
||
self.assertRaises(ValueError, self.game.roll, -11) | ||
|
||
def test_a_roll_cannot_score_more_than_10_points(self): | ||
|
||
self.assertRaises(ValueError, self.game.roll, 11) | ||
|
||
def test_two_rolls_in_a_frame_cannot_score_more_than_10_points(self): | ||
self.game.roll(5) | ||
|
||
self.assertRaises(ValueError, self.game.roll, 6) | ||
|
||
def test_bonus_after_strike_in_last_frame_cannot_score_more_than_10(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10] | ||
|
||
self.roll(rolls) | ||
|
||
self.assertRaises(ValueError, self.game.roll, 11) | ||
|
||
def test_bonus_aft_last_frame_strk_can_be_more_than_10_if_1_is_strk(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, | ||
10, 6] | ||
|
||
score = self.roll_and_score(rolls) | ||
|
||
self.assertEqual(score, 26) | ||
|
||
def test_bonus_aft_last_frame_strk_cnt_be_strk_if_first_is_not_strk(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 6] | ||
|
||
self.roll(rolls) | ||
|
||
self.assertRaises(ValueError, self.game.roll, 10) | ||
|
||
def test_an_incomplete_game_cannot_be_scored(self): | ||
rolls = [0, 0] | ||
|
||
self.roll(rolls) | ||
|
||
self.assertRaises(IndexError, self.game.score) | ||
|
||
def test_cannot_roll_if_there_are_already_ten_frames(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | ||
|
||
self.roll(rolls) | ||
|
||
self.assertRaises(IndexError, self.game.roll, 0) | ||
|
||
def test_bonus_rolls_for_strike_must_be_rolled_before_score_is_calc(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10] | ||
|
||
self.roll(rolls) | ||
|
||
self.assertRaises(IndexError, self.game.score) | ||
|
||
def test_both_bonuses_for_strike_must_be_rolled_before_score(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10] | ||
|
||
self.roll(rolls) | ||
|
||
self.assertRaises(IndexError, self.game.score) | ||
|
||
def test_bonus_rolls_for_spare_must_be_rolled_before_score_is_calc(self): | ||
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3] | ||
|
||
self.roll(rolls) | ||
|
||
self.assertRaises(IndexError, self.game.score) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
MAX_PINS = 10 | ||
NUM_FRAMES = 10 | ||
|
||
|
||
class BowlingGame(object): | ||
"""This class manages the Bowling Game including the roll and score | ||
methods""" | ||
def __init__(self): | ||
self.rolls = [] | ||
self.totalScore = 0 | ||
self.currentFrame = Frame() | ||
self.bonusRollsAccrued = 0 | ||
self.bonusRollsSeen = 0 | ||
|
||
def roll(self, pins): | ||
if self.isBonusRoll(): | ||
self.bonusRollsSeen += 1 | ||
|
||
# is the second roll valid based off the first? | ||
if (self.currentFrame.isOpen() and | ||
self.currentFrame.getFrame()[0] is not None): | ||
if self.currentFrame.getFrame()[0] + pins > MAX_PINS: | ||
raise ValueError("This roll will cause the current frame " | ||
"to be getter than the max number of pins") | ||
|
||
# open a new frame if the last one has been closed | ||
if not self.currentFrame.isOpen(): | ||
self.currentFrame = Frame() | ||
|
||
# valid roll between 0-10 | ||
if pins in range(MAX_PINS + 1): | ||
# raise an error if the game is over and they try to roll again | ||
if ((len(self.rolls) == NUM_FRAMES) and | ||
self.bonusRollsAccrued == 0): | ||
raise IndexError("Max Frames have been reached. Too many " | ||
"rolls") | ||
else: | ||
self.currentFrame.roll(pins, | ||
self.isBonusRoll(), | ||
self.bonusRollsAccrued, | ||
self.bonusRollsSeen) | ||
# if we closed it add it to our rolls | ||
if not self.currentFrame.isOpen(): | ||
self.rolls.append(self.currentFrame) | ||
# If this is the last frame did we earn any bonus rolls? | ||
if len(self.rolls) == NUM_FRAMES: | ||
self.bonusRollsEarned() | ||
else: | ||
raise ValueError("Amount of pins rolled is greater than the max " | ||
"number of pins") | ||
|
||
def score(self): | ||
frame_index = 0 | ||
|
||
while (frame_index <= NUM_FRAMES-1): | ||
frame = self.rolls[frame_index].getFrame() | ||
|
||
roll1 = frame[0] | ||
roll2 = frame[1] | ||
|
||
if self.isStrike(roll1): | ||
self.totalScore += roll1 + self.stikeBonus(frame_index) | ||
else: | ||
if self.isSpare(roll1, roll2): | ||
self.totalScore += roll1 + roll2 + \ | ||
self.spareBonus(frame_index) | ||
else: | ||
self.totalScore += roll1 + roll2 | ||
|
||
frame_index += 1 | ||
|
||
return self.totalScore | ||
|
||
def isStrike(self, pins): | ||
return True if pins == MAX_PINS else False | ||
|
||
def isSpare(self, roll1, roll2): | ||
return True if roll1 + roll2 == MAX_PINS else False | ||
|
||
def stikeBonus(self, frame_index): | ||
bonusroll1 = self.rolls[frame_index+1].getFrame()[0] | ||
bonusroll2 = 0 | ||
# need to go further out if the next on is a strike | ||
if bonusroll1 == 10: | ||
bonusroll2 = self.rolls[frame_index+2].getFrame()[0] | ||
else: | ||
bonusroll2 = self.rolls[frame_index+1].getFrame()[1] | ||
# edge case - if the last roll is a stike the bonus rolls needs to be | ||
# validated | ||
if (not self.isStrike(bonusroll1) and | ||
(bonusroll1 + bonusroll2 > MAX_PINS)): | ||
raise ValueError("The bonus rolls total to greater than the max " | ||
"number of pins") | ||
else: | ||
return bonusroll1 + bonusroll2 | ||
|
||
def spareBonus(self, frame_index): | ||
return self.rolls[frame_index+1].getFrame()[0] | ||
|
||
def isLastFrame(self, frame_index): | ||
return True if frame_index >= len(self.rolls)-1 else False | ||
|
||
def bonusRollsEarned(self): | ||
if len(self.rolls) == NUM_FRAMES: | ||
lastFrame = self.rolls[NUM_FRAMES-1].getFrame() | ||
if self.isStrike(lastFrame[0]): | ||
self.bonusRollsAccrued = 2 | ||
elif self.isSpare(lastFrame[0], lastFrame[1]): | ||
self.bonusRollsAccrued = 1 | ||
else: | ||
self.bonusRollsAccrued = 0 | ||
return | ||
|
||
def isBonusRoll(self): | ||
# if we've already seen all | ||
return True if len(self.rolls) >= NUM_FRAMES else False | ||
|
||
|
||
class Frame(object): | ||
"""This class is for internal use only. It divides up the array of | ||
rolls into Frame objects""" | ||
def __init__(self): | ||
self.rolls = [None, None] | ||
self.open = True | ||
|
||
def roll(self, roll, bonusRoll, accruedBonuses, seenBonuses): | ||
# if it's a strike we close the frame | ||
if roll == 10: | ||
self.rolls[0] = 10 | ||
self.rolls[1] = 0 | ||
self.open = False | ||
else: | ||
# first roll, but frame is still open | ||
if self.rolls[0] is None: | ||
self.rolls[0] = roll | ||
# may need to close bonus roll frames before 2 have been seen | ||
if bonusRoll and seenBonuses == accruedBonuses: | ||
self.rolls[1] = 0 | ||
self.open = False | ||
else: | ||
# second roll, closes frame | ||
self.rolls[1] = roll | ||
self.open = False | ||
|
||
def isOpen(self): | ||
return self.open | ||
|
||
def getFrame(self): | ||
return self.rolls |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this doesn't look right, method name doesn't correspond to its actual function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you mean on this one. you call isOpen to find out if the frame is open (true) or not (false)