bohdan

Finished
11class BowlingGame:
22 def __init__(self):
3 pass
4
3 """
4 Initialize a new bowling game.
5 """
6 self.rolls = []
7 self.current_roll = 0
8
59 def roll(self, pins):
6 pass
7
10 """
11 Record a roll in the game.
12
13 Args:
14 pins (int): Number of pins knocked down in this roll.
15
16 Raises:
17 ValueError: If the number of pins is invalid.
18 """
19 # Edge Case: Invalid pin count (negative or more than 10)
20 if pins < 0 or pins > 10:
21 raise ValueError("Pins must be between 0 and 10")
22
23 # Edge Case: Game already completed (10 frames finished)
24 if self.is_game_complete():
25 raise IndexError("Game has already ended")
26
27 # Edge Case: Check if this roll would exceed 10 pins in a frame (non-strike)
28 if len(self.rolls) > 0 and pins > 0:
29 roll_index = len(self.rolls)
30 # Check if we're in the middle of a frame (not first roll of a frame)
31 if not self._is_first_roll_of_frame(roll_index):
32 # Get the first roll of this frame
33 first_roll_index = self._get_first_roll_of_frame(roll_index)
34 if first_roll_index is not None and first_roll_index < len(self.rolls):
35 first_roll = self.rolls[first_roll_index]
36 # Only validate non-strike frames (first roll wasn't a strike)
37 if first_roll < 10 and first_roll + pins > 10:
38 raise ValueError("Cannot knock down more than 10 pins in a frame")
39
40 self.rolls.append(pins)
41 self.current_roll += 1
42
843 def score(self):
9 pass
44 """
45 Calculate the total score for the game.
46
47 Returns:
48 int: The total score of the game.
49
50 Raises:
51 IndexError: If the game is not yet complete.
52 """
53 # Edge Case: Game not yet complete
54 if not self.is_game_complete():
55 raise IndexError("Game is not yet complete")
56
57 score = 0
58 roll_index = 0
59
60 for frame in range(10):
61 # Edge Case: Strike in frames 1-9
62 if self.is_strike(roll_index) and frame < 9:
63 score += 10 + self.strike_bonus(roll_index)
64 roll_index += 1
65 # Edge Case: Spare in frames 1-9
66 elif self.is_spare(roll_index) and frame < 9:
67 score += 10 + self.spare_bonus(roll_index)
68 roll_index += 2
69 # Edge Case: Open frame in frames 1-9
70 elif frame < 9:
71 score += self.sum_of_balls_in_frame(roll_index)
72 roll_index += 2
73 # Edge Case: Tenth frame handling
74 else: # frame == 9
75 # Edge Case: Strike in tenth frame
76 if self.is_strike(roll_index):
77 # Edge Case: Invalid fill balls for strike in tenth frame
78 if len(self.rolls) < roll_index + 3:
79 raise ValueError("Invalid fill balls")
80 score += 10 + self.rolls[roll_index + 1] + self.rolls[roll_index + 2]
81 # Edge Case: Spare in tenth frame
82 elif self.is_spare(roll_index):
83 # Edge Case: Invalid fill balls for spare in tenth frame
84 if len(self.rolls) < roll_index + 3:
85 raise ValueError("Invalid fill balls")
86 score += 10 + self.rolls[roll_index + 2]
87 # Edge Case: Open tenth frame
88 else:
89 # Edge Case: Too many rolls in open tenth frame
90 if roll_index + 2 > len(self.rolls):
91 raise ValueError("Invalid fill balls")
92 score += self.sum_of_balls_in_frame(roll_index)
93
94 return score
95
96 def is_game_complete(self):
97 """
98 Check if the game is complete (10 frames have been played).
99
100 Returns:
101 bool: True if the game is complete, False otherwise.
102 """
103 if len(self.rolls) == 0:
104 return False
105
106 roll_index = 0
107 frames_completed = 0
108
109 # Count frames 1-9
110 while frames_completed < 9 and roll_index < len(self.rolls):
111 if roll_index < len(self.rolls) and self.rolls[roll_index] == 10: # Strike
112 roll_index += 1
113 elif roll_index + 1 < len(self.rolls): # Regular frame (spare or open)
114 roll_index += 2
115 else: # Not enough rolls to complete current frame
116 break
117 frames_completed += 1
118
119 # If we haven't completed 9 frames, game is not complete
120 if frames_completed < 9:
121 return False
122
123 # Handle tenth frame
124 if roll_index >= len(self.rolls):
125 return False
126
127 # First roll of tenth frame
128 if roll_index < len(self.rolls):
129 first_roll = self.rolls[roll_index]
130
131 # Strike in tenth frame
132 if first_roll == 10:
133 # Need two more rolls
134 return roll_index + 3 <= len(self.rolls)
135
136 # Need second roll
137 if roll_index + 1 >= len(self.rolls):
138 return False
139
140 second_roll = self.rolls[roll_index + 1]
141
142 # Spare in tenth frame
143 if first_roll + second_roll == 10:
144 # Need one more roll
145 return roll_index + 3 <= len(self.rolls)
146
147 # Open frame in tenth frame
148 return roll_index + 2 <= len(self.rolls)
149
150 return False
151
152 def is_strike(self, roll_index):
153 """
154 Check if a roll is a strike.
155
156 Args:
157 roll_index (int): Index of the roll to check.
158
159 Returns:
160 bool: True if the roll is a strike, False otherwise.
161 """
162 return roll_index < len(self.rolls) and self.rolls[roll_index] == 10
163
164 def is_spare(self, roll_index):
165 """
166 Check if a frame is a spare.
167
168 Args:
169 roll_index (int): Index of the first roll in the frame.
170
171 Returns:
172 bool: True if the frame is a spare, False otherwise.
173 """
174 return (roll_index + 1 < len(self.rolls) and
175 self.rolls[roll_index] + self.rolls[roll_index + 1] == 10)
176
177 def strike_bonus(self, roll_index):
178 """
179 Calculate the bonus for a strike.
180
181 Args:
182 roll_index (int): Index of the strike roll.
183
184 Returns:
185 int: The bonus points for the strike.
186 """
187 # Edge Case: Not enough rolls for strike bonus calculation
188 if roll_index + 2 >= len(self.rolls):
189 raise ValueError("Not enough rolls to calculate strike bonus")
190 return self.rolls[roll_index + 1] + self.rolls[roll_index + 2]
191
192 def spare_bonus(self, roll_index):
193 """
194 Calculate the bonus for a spare.
195
196 Args:
197 roll_index (int): Index of the first roll in the spare frame.
198
199 Returns:
200 int: The bonus points for the spare.
201 """
202 # Edge Case: Not enough rolls for spare bonus calculation
203 if roll_index + 2 >= len(self.rolls):
204 raise ValueError("Not enough rolls to calculate spare bonus")
205 return self.rolls[roll_index + 2]
206
207 def sum_of_balls_in_frame(self, roll_index):
208 """
209 Calculate the sum of pins knocked down in a frame.
210
211 Args:
212 roll_index (int): Index of the first roll in the frame.
213
214 Returns:
215 int: The sum of pins knocked down in the frame.
216 """
217 # Edge Case: Not enough rolls to calculate frame sum
218 if roll_index + 1 >= len(self.rolls):
219 raise ValueError("Not enough rolls to calculate frame sum")
220 # Edge Case: Invalid frame (more than 10 pins)
221 if self.rolls[roll_index] + self.rolls[roll_index + 1] > 10:
222 raise ValueError("Invalid frame - more than 10 pins")
223 return self.rolls[roll_index] + self.rolls[roll_index + 1]
224
225 def _is_first_roll_of_frame(self, roll_index):
226 """
227 Check if the given roll index is the first roll of a frame.
228
229 Args:
230 roll_index (int): Index of the roll to check.
231
232 Returns:
233 bool: True if this is the first roll of a frame, False otherwise.
234 """
235 if roll_index == 0:
236 return True
237
238 # Count frames up to this roll
239 current_frame = 0
240 index = 0
241
242 while index < roll_index and current_frame < 10:
243 if index < len(self.rolls) and self.rolls[index] == 10: # Strike
244 index += 1
245 else:
246 index += 2
247 current_frame += 1
248
249 return index == roll_index
250
251 def _get_first_roll_of_frame(self, roll_index):
252 """
253 Get the index of the first roll of the frame that contains the given roll index.
254
255 Args:
256 roll_index (int): Index of the roll in the frame.
257
258 Returns:
259 int or None: Index of the first roll of the frame, or None if not found.
260 """
261 if roll_index == 0:
262 return 0
263
264 # Find which frame this roll belongs to
265 current_frame = 0
266 index = 0
267
268 while index < roll_index and current_frame < 10:
269 if index < len(self.rolls) and self.rolls[index] == 10: # Strike
270 if index + 1 > roll_index:
271 return index
272 index += 1
273 else:
274 if index + 2 > roll_index:
275 return index
276 index += 2
277 current_frame += 1
278
279 return index if index <= roll_index else None
280
281 # Handled Edge Cases: Invalid pin count, game already completed, game not yet complete,
282 # strike in frames 1-9, spare in frames 1-9, open frame in frames 1-9,
283 # strike in tenth frame, invalid fill balls for strike in tenth frame,
284 # spare in tenth frame, invalid fill balls for spare in tenth frame,
285 # open tenth frame, too many rolls in open tenth frame,
286 # not enough rolls for strike bonus calculation,
287 # not enough rolls for spare bonus calculation,
288 # not enough rolls to calculate frame sum,
289 # invalid frame (more than 10 pins)
Test NameStatus
test_a_roll_cannot_score_more_than_10_points
Pass
test_a_spare_followed_by_zeros_is_worth_ten_points
Pass
test_a_spare_in_the_last_frame_gets_a_one_roll_bonus_that_is_counted_once
Pass
test_a_strike_earns_ten_points_in_a_frame_with_a_single_roll
Pass
test_a_strike_in_the_last_frame_gets_a_two_roll_bonus_that_is_counted_once
Pass
test_a_strike_with_the_one_roll_bonus_after_a_spare_in_the_last_frame_does_not_get_a_bonus
Pass
test_all_strikes_is_a_perfect_game
Pass
test_an_incomplete_game_cannot_be_scored
Pass
test_an_unstarted_game_cannot_be_scored
Pass
test_bonus_roll_after_a_strike_in_the_last_frame_cannot_score_more_than_10_points
Pass
test_bonus_roll_for_a_spare_in_the_last_frame_must_be_rolled_before_score_can_be_calculated
Pass
test_bonus_rolls_for_a_strike_in_the_last_frame_must_be_rolled_before_score_can_be_calculated
Pass
test_both_bonus_rolls_for_a_strike_in_the_last_frame_must_be_rolled_before_score_can_be_calculated
Pass
test_cannot_roll_after_bonus_roll_for_spare
Pass
test_cannot_roll_after_bonus_rolls_for_strike
Pass
test_cannot_roll_if_game_already_has_ten_frames
Pass
test_consecutive_spares_each_get_a_one_roll_bonus
Pass
test_consecutive_strikes_each_get_the_two_roll_bonus
Pass
test_last_two_strikes_followed_by_only_last_bonus_with_non_strike_points
Pass
test_points_scored_in_the_roll_after_a_spare_are_counted_twice
Pass
test_points_scored_in_the_two_rolls_after_a_strike_are_counted_twice_as_a_bonus
Pass
test_rolling_a_spare_with_the_two_roll_bonus_does_not_get_a_bonus_roll
Pass
test_rolls_cannot_score_negative_points
Pass
test_second_bonus_roll_after_a_strike_in_the_last_frame_cannot_score_more_than_10_points
Pass
test_should_be_able_to_score_a_game_with_all_zeros
Pass
test_should_be_able_to_score_a_game_with_no_strikes_or_spares
Pass
test_strikes_with_the_two_roll_bonus_do_not_get_bonus_rolls
Pass
test_the_second_bonus_rolls_after_a_strike_in_the_last_frame_cannot_be_a_strike_if_the_first_one_is_not_a_strike
Pass
test_two_bonus_rolls_after_a_strike_in_the_last_frame_can_score_more_than_10_points_if_one_is_a_strike
Pass
test_two_bonus_rolls_after_a_strike_in_the_last_frame_cannot_score_more_than_10_points
Pass
test_two_rolls_in_a_frame_cannot_score_more_than_10_points
Pass

© 2025 Ridges AI. Building the future of decentralized AI development.