Skip to content

Commit f6ae46b

Browse files
thecouchcodercmccandless
authored andcommitted
[WIP] Implement exercise bowling (#790)
* Implement exercise bowling * bowling: created the files * bowling: implemented test for an all 0 game * bowling: passing all zeros test * bowling tested and implemented logic for spares * bowling: bonus roll implemented * bowling: heavily refactored code. Now passing 9 tests * bowling: tested and implemeneted first 14 tests * fix travis issues with line length * bowling: setup config * bowling: passing 19 tests * bowling: fixed error in config.json * bowling: heavily refactoring code to implement frame logic during roll WIP * bowling: still alot of refactoring going on and alot is broken * bowling: still refactoring and still alot of errors, but closer * bowling: nearly done refactoring * bowling: finally back to passing all tests like before refactor * bowling: added a few more tests and cleaned up after refactor * bowling: passes all tests * bowling: fixing travis issue * bowling: fixes as requested * bowling: fixes as requested * bowling: Travis fixes * bowling: Travis fixes * bowling: Travis fixes * bowling: Travis fixes * bowling: fixes as requested
1 parent 75056df commit f6ae46b

File tree

5 files changed

+365
-0
lines changed

5 files changed

+365
-0
lines changed

config.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@
8080
"transforming"
8181
]
8282
},
83+
{
84+
"uuid": "ca970fee-71b4-41e1-a5c3-b23bf574eb33",
85+
"slug": "bowling",
86+
"core": false,
87+
"unlocked_by": null,
88+
"difficulty": 5,
89+
"topics": [
90+
"classes",
91+
"exception_handling",
92+
"logic"
93+
]
94+
},
8395
{
8496
"uuid": "7961c852-c87a-44b0-b152-efea3ac8555c",
8597
"slug": "isbn-verifier",

exercises/bowling/.gitignore

Whitespace-only changes.

exercises/bowling/bowling.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
3+
class BowlingGame(object):
4+
def __init__(self):
5+
pass
6+
7+
def roll(self, pins):
8+
pass
9+
10+
def score(self):
11+
pass

exercises/bowling/bowling_test.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import unittest
2+
3+
from bowling import BowlingGame
4+
5+
6+
# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.1
7+
8+
class BowlingTests(unittest.TestCase):
9+
def setUp(self):
10+
self.game = BowlingGame()
11+
12+
def roll(self, rolls):
13+
[self.game.roll(roll) for roll in rolls]
14+
15+
def roll_and_score(self, rolls):
16+
self.roll(rolls)
17+
return self.game.score()
18+
19+
def test_should_be_able_to_score_a_game_with_all_zeros(self):
20+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
21+
22+
score = self.roll_and_score(rolls)
23+
24+
self.assertEqual(score, 0)
25+
26+
def test_should_be_able_to_score_a_game_with_no_strikes_or_spares(self):
27+
rolls = [3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6]
28+
29+
score = self.roll_and_score(rolls)
30+
31+
self.assertEqual(score, 90)
32+
33+
def test_a_spare_follow_by_zeros_is_worth_ten_points(self):
34+
rolls = [6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
35+
36+
score = self.roll_and_score(rolls)
37+
38+
self.assertEqual(score, 10)
39+
40+
def test_points_scored_in_the_roll_after_a_spare_are_counted_twice(self):
41+
rolls = [6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
42+
43+
score = self.roll_and_score(rolls)
44+
45+
self.assertEqual(score, 16)
46+
47+
def test_consecutive_spares_each_get_a_one_roll_bonus(self):
48+
rolls = [5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
49+
50+
score = self.roll_and_score(rolls)
51+
52+
self.assertEqual(score, 31)
53+
54+
def test_last_frame_spare_gets_bonus_roll_that_is_counted_twice(self):
55+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7]
56+
57+
score = self.roll_and_score(rolls)
58+
59+
self.assertEqual(score, 17)
60+
61+
def test_a_strike_earns_ten_points_in_a_frame_with_a_single_roll(self):
62+
rolls = [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
63+
64+
score = self.roll_and_score(rolls)
65+
66+
self.assertEqual(score, 10)
67+
68+
def test_two_rolls_points_after_strike_are_counted_twice(self):
69+
rolls = [10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
70+
71+
score = self.roll_and_score(rolls)
72+
73+
self.assertEqual(score, 26)
74+
75+
def test_consecutive_stikes_each_get_the_two_roll_bonus(self):
76+
rolls = [10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
77+
78+
score = self.roll_and_score(rolls)
79+
80+
self.assertEqual(score, 81)
81+
82+
def test_strike_in_last_frame_gets_two_roll_bonus_counted_once(self):
83+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
84+
10, 7, 1]
85+
86+
score = self.roll_and_score(rolls)
87+
88+
self.assertEqual(score, 18)
89+
90+
def test_rolling_spare_with_bonus_roll_does_not_get_bonus(self):
91+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
92+
0, 10, 7, 3]
93+
94+
score = self.roll_and_score(rolls)
95+
96+
self.assertEqual(score, 20)
97+
98+
def test_strikes_with_the_two_bonus_rolls_do_not_get_bonus_rolls(self):
99+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10,
100+
10, 10]
101+
102+
score = self.roll_and_score(rolls)
103+
104+
self.assertEqual(score, 30)
105+
106+
def test_strike_with_bonus_after_spare_in_last_frame_gets_no_bonus(self):
107+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7,
108+
3, 10]
109+
110+
score = self.roll_and_score(rolls)
111+
112+
self.assertEqual(score, 20)
113+
114+
def test_all_strikes_is_a_perfect_game(self):
115+
rolls = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
116+
117+
score = self.roll_and_score(rolls)
118+
119+
self.assertEqual(score, 300)
120+
121+
def test_rolls_cannot_score_negative_points(self):
122+
123+
self.assertRaises(ValueError, self.game.roll, -11)
124+
125+
def test_a_roll_cannot_score_more_than_10_points(self):
126+
127+
self.assertRaises(ValueError, self.game.roll, 11)
128+
129+
def test_two_rolls_in_a_frame_cannot_score_more_than_10_points(self):
130+
self.game.roll(5)
131+
132+
self.assertRaises(ValueError, self.game.roll, 6)
133+
134+
def test_bonus_after_strike_in_last_frame_cannot_score_more_than_10(self):
135+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10]
136+
137+
self.roll(rolls)
138+
139+
self.assertRaises(ValueError, self.game.roll, 11)
140+
141+
def test_bonus_aft_last_frame_strk_can_be_more_than_10_if_1_is_strk(self):
142+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10,
143+
10, 6]
144+
145+
score = self.roll_and_score(rolls)
146+
147+
self.assertEqual(score, 26)
148+
149+
def test_bonus_aft_last_frame_strk_cnt_be_strk_if_first_is_not_strk(self):
150+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 6]
151+
152+
self.roll(rolls)
153+
154+
self.assertRaises(ValueError, self.game.roll, 10)
155+
156+
def test_an_incomplete_game_cannot_be_scored(self):
157+
rolls = [0, 0]
158+
159+
self.roll(rolls)
160+
161+
self.assertRaises(IndexError, self.game.score)
162+
163+
def test_cannot_roll_if_there_are_already_ten_frames(self):
164+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
165+
166+
self.roll(rolls)
167+
168+
self.assertRaises(IndexError, self.game.roll, 0)
169+
170+
def test_bonus_rolls_for_strike_must_be_rolled_before_score_is_calc(self):
171+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10]
172+
173+
self.roll(rolls)
174+
175+
self.assertRaises(IndexError, self.game.score)
176+
177+
def test_both_bonuses_for_strike_must_be_rolled_before_score(self):
178+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10]
179+
180+
self.roll(rolls)
181+
182+
self.assertRaises(IndexError, self.game.score)
183+
184+
def test_bonus_rolls_for_spare_must_be_rolled_before_score_is_calc(self):
185+
rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3]
186+
187+
self.roll(rolls)
188+
189+
self.assertRaises(IndexError, self.game.score)
190+
191+
192+
if __name__ == '__main__':
193+
unittest.main()

exercises/bowling/example.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
MAX_PINS = 10
2+
NUM_FRAMES = 10
3+
4+
5+
class BowlingGame(object):
6+
"""This class manages the Bowling Game including the roll and score
7+
methods"""
8+
def __init__(self):
9+
self.rolls = []
10+
self.totalScore = 0
11+
self.currentFrame = Frame()
12+
self.bonusRollsAccrued = 0
13+
self.bonusRollsSeen = 0
14+
15+
def roll(self, pins):
16+
if self.isBonusRoll():
17+
self.bonusRollsSeen += 1
18+
19+
# is the second roll valid based off the first?
20+
if (self.currentFrame.isOpen() and
21+
self.currentFrame.getFrame()[0] is not None):
22+
if self.currentFrame.getFrame()[0] + pins > MAX_PINS:
23+
raise ValueError("This roll will cause the current frame "
24+
"to be getter than the max number of pins")
25+
26+
# open a new frame if the last one has been closed
27+
if not self.currentFrame.isOpen():
28+
self.currentFrame = Frame()
29+
30+
# valid roll between 0-10
31+
if pins in range(MAX_PINS + 1):
32+
# raise an error if the game is over and they try to roll again
33+
if ((len(self.rolls) == NUM_FRAMES) and
34+
self.bonusRollsAccrued == 0):
35+
raise IndexError("Max Frames have been reached. Too many "
36+
"rolls")
37+
else:
38+
self.currentFrame.roll(pins,
39+
self.isBonusRoll(),
40+
self.bonusRollsAccrued,
41+
self.bonusRollsSeen)
42+
# if we closed it add it to our rolls
43+
if not self.currentFrame.isOpen():
44+
self.rolls.append(self.currentFrame)
45+
# If this is the last frame did we earn any bonus rolls?
46+
if len(self.rolls) == NUM_FRAMES:
47+
self.bonusRollsEarned()
48+
else:
49+
raise ValueError("Amount of pins rolled is greater than the max "
50+
"number of pins")
51+
52+
def score(self):
53+
frame_index = 0
54+
55+
while (frame_index <= NUM_FRAMES-1):
56+
frame = self.rolls[frame_index].getFrame()
57+
58+
roll1 = frame[0]
59+
roll2 = frame[1]
60+
61+
if self.isStrike(roll1):
62+
self.totalScore += roll1 + self.stikeBonus(frame_index)
63+
else:
64+
if self.isSpare(roll1, roll2):
65+
self.totalScore += roll1 + roll2 + \
66+
self.spareBonus(frame_index)
67+
else:
68+
self.totalScore += roll1 + roll2
69+
70+
frame_index += 1
71+
72+
return self.totalScore
73+
74+
def isStrike(self, pins):
75+
return True if pins == MAX_PINS else False
76+
77+
def isSpare(self, roll1, roll2):
78+
return True if roll1 + roll2 == MAX_PINS else False
79+
80+
def stikeBonus(self, frame_index):
81+
bonusroll1 = self.rolls[frame_index+1].getFrame()[0]
82+
bonusroll2 = 0
83+
# need to go further out if the next on is a strike
84+
if bonusroll1 == 10:
85+
bonusroll2 = self.rolls[frame_index+2].getFrame()[0]
86+
else:
87+
bonusroll2 = self.rolls[frame_index+1].getFrame()[1]
88+
# edge case - if the last roll is a stike the bonus rolls needs to be
89+
# validated
90+
if (not self.isStrike(bonusroll1) and
91+
(bonusroll1 + bonusroll2 > MAX_PINS)):
92+
raise ValueError("The bonus rolls total to greater than the max "
93+
"number of pins")
94+
else:
95+
return bonusroll1 + bonusroll2
96+
97+
def spareBonus(self, frame_index):
98+
return self.rolls[frame_index+1].getFrame()[0]
99+
100+
def isLastFrame(self, frame_index):
101+
return True if frame_index >= len(self.rolls)-1 else False
102+
103+
def bonusRollsEarned(self):
104+
if len(self.rolls) == NUM_FRAMES:
105+
lastFrame = self.rolls[NUM_FRAMES-1].getFrame()
106+
if self.isStrike(lastFrame[0]):
107+
self.bonusRollsAccrued = 2
108+
elif self.isSpare(lastFrame[0], lastFrame[1]):
109+
self.bonusRollsAccrued = 1
110+
else:
111+
self.bonusRollsAccrued = 0
112+
return
113+
114+
def isBonusRoll(self):
115+
# if we've already seen all
116+
return True if len(self.rolls) >= NUM_FRAMES else False
117+
118+
119+
class Frame(object):
120+
"""This class is for internal use only. It divides up the array of
121+
rolls into Frame objects"""
122+
def __init__(self):
123+
self.rolls = [None, None]
124+
self.open = True
125+
126+
def roll(self, roll, bonusRoll, accruedBonuses, seenBonuses):
127+
# if it's a strike we close the frame
128+
if roll == 10:
129+
self.rolls[0] = 10
130+
self.rolls[1] = 0
131+
self.open = False
132+
else:
133+
# first roll, but frame is still open
134+
if self.rolls[0] is None:
135+
self.rolls[0] = roll
136+
# may need to close bonus roll frames before 2 have been seen
137+
if bonusRoll and seenBonuses == accruedBonuses:
138+
self.rolls[1] = 0
139+
self.open = False
140+
else:
141+
# second roll, closes frame
142+
self.rolls[1] = roll
143+
self.open = False
144+
145+
def isOpen(self):
146+
return self.open
147+
148+
def getFrame(self):
149+
return self.rolls

0 commit comments

Comments
 (0)