-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[WIP] Implement exercise bowling #790
Changes from 26 commits
c382caa
ad21e2a
1e16f9e
5b29088
028ce96
5b61ca4
4527240
1785b1f
73c311f
3eb75d1
9fb30a7
090300d
ff70fd5
293bf44
0cd0fc1
3f1777e
55c3c6c
53b6e52
be6cf1c
91d1cf0
6034a95
511732a
5298cbb
d3c349d
833dce2
1cf050f
5f8b980
dca0f0e
95b7666
de5a2bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.1 | ||
import unittest | ||
|
||
from bowling import BowlingGame | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please also leave a comment stating what version of
|
||
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() |
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. docstring here would be nice 📓 |
||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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) |
||
return self.open | ||
|
||
def getFrame(self): | ||
return self.rolls |
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.
For consistency with the other exercises, could you please move this line like so:
Please note the number of blank lines before and after.
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.
done!