| 1 | 1 | class BowlingGame: |
| 2 | 2 | def __init__(self): |
| 3 | | pass |
| 3 | """ |
| 4 | Initialize a new bowling game. |
| 5 | """ |
| 6 | self.rolls = [] |
| 4 | 7 | |
| 5 | 8 | def roll(self, pins): |
| 6 | | pass |
| 9 | """ |
| 10 | Record a roll in the game. |
| 11 | |
| 12 | Args: |
| 13 | pins (int): Number of pins knocked down in this roll |
| 14 | |
| 15 | Raises: |
| 16 | ValueError: If the number of pins is invalid or game is already finished |
| 17 | """ |
| 18 | # Edge Case: Game is already finished |
| 19 | if self._is_game_finished(): |
| 20 | raise ValueError("Cannot roll after game is over") |
| 21 | |
| 22 | # Edge Case: Invalid number of pins (negative or more than 10) |
| 23 | if pins < 0 or pins > 10: |
| 24 | if pins < 0: |
| 25 | raise ValueError("Negative roll is invalid") |
| 26 | else: |
| 27 | raise ValueError("Pins cannot be greater than 10") |
| 28 | |
| 29 | # Check if we're trying to roll after a strike in the same frame (only for non-10th frames) |
| 30 | # Count frames to determine if we're in the same frame as the last strike |
| 31 | frame_count = 0 |
| 32 | roll_index = 0 |
| 33 | |
| 34 | # Process frames 1-9 |
| 35 | while frame_count < 9 and roll_index < len(self.rolls): |
| 36 | if self._is_strike(self.rolls[roll_index]): |
| 37 | # Strike frame - completed with 1 roll |
| 38 | roll_index += 1 |
| 39 | frame_count += 1 |
| 40 | else: |
| 41 | # Non-strike frame - needs 2 rolls |
| 42 | if roll_index + 1 < len(self.rolls): |
| 43 | # Complete non-strike frame |
| 44 | roll_index += 2 |
| 45 | frame_count += 1 |
| 46 | else: |
| 47 | # Incomplete non-strike frame - this is where we would add the roll |
| 48 | break |
| 49 | |
| 50 | # If we're in a frame that already had a strike, and we're trying to add another roll to it |
| 51 | if frame_count < 9 and roll_index > 0 and roll_index == len(self.rolls) and self._is_strike(self.rolls[roll_index - 1]): |
| 52 | raise ValueError("Cannot throw after a strike in the same frame") |
| 53 | |
| 54 | # Validate before adding the roll |
| 55 | if not self._is_valid_roll_to_add(pins): |
| 56 | raise ValueError("Invalid roll: pins downed in a frame cannot exceed 10") |
| 57 | |
| 58 | # Add the roll |
| 59 | self.rolls.append(pins) |
| 60 | |
| 61 | def _is_valid_roll_to_add(self, pins): |
| 62 | """ |
| 63 | Validate if adding a new roll would be valid according to bowling rules. |
| 64 | |
| 65 | Args: |
| 66 | pins (int): Number of pins to be knocked down |
| 67 | |
| 68 | Returns: |
| 69 | bool: True if the roll would be valid, False otherwise |
| 70 | """ |
| 71 | # Check if we're trying to add a roll that would create an invalid frame structure |
| 72 | # Specifically, check if we're trying to add a second roll to a frame that already had a strike |
| 73 | |
| 74 | # Count completed frames (frames 1-9) |
| 75 | frame_count = 0 |
| 76 | roll_index = 0 |
| 77 | |
| 78 | # Process frames 1-9 |
| 79 | while frame_count < 9 and roll_index < len(self.rolls): |
| 80 | if self._is_strike(self.rolls[roll_index]): |
| 81 | # Strike frame - completed with 1 roll |
| 82 | roll_index += 1 |
| 83 | frame_count += 1 |
| 84 | else: |
| 85 | # Non-strike frame - needs 2 rolls |
| 86 | if roll_index + 1 < len(self.rolls): |
| 87 | # Complete non-strike frame |
| 88 | roll_index += 2 |
| 89 | frame_count += 1 |
| 90 | else: |
| 91 | # Incomplete non-strike frame - check if adding this roll would exceed 10 pins |
| 92 | if self.rolls[roll_index] + pins > 10: |
| 93 | return False |
| 94 | return True |
| 95 | |
| 96 | # If we've completed 9 frames, we're in the 10th frame |
| 97 | # The 10th frame has special rules |
| 98 | if frame_count >= 9: |
| 99 | # In 10th frame, check if we have valid rolls so far |
| 100 | tenth_frame_rolls = len(self.rolls) - roll_index |
| 101 | |
| 102 | if tenth_frame_rolls == 0: |
| 103 | # First roll of 10th frame - always valid |
| 104 | return True |
| 105 | elif tenth_frame_rolls == 1: |
| 106 | # Second roll of 10th frame |
| 107 | first_roll = self.rolls[roll_index] |
| 108 | if first_roll == 10: |
| 109 | # First roll was a strike - second roll is valid (first fill ball) |
| 110 | return True |
| 111 | else: |
| 112 | # First roll was not a strike - check if total exceeds 10 |
| 113 | if first_roll + pins > 10: |
| 114 | return False |
| 115 | return True |
| 116 | elif tenth_frame_rolls == 2: |
| 117 | # Third roll of 10th frame |
| 118 | first_roll = self.rolls[roll_index] |
| 119 | second_roll = self.rolls[roll_index + 1] |
| 120 | |
| 121 | if first_roll == 10: |
| 122 | # First roll was a strike |
| 123 | if second_roll == 10: |
| 124 | # Two strikes - third roll is second fill ball |
| 125 | return True |
| 126 | else: |
| 127 | # Strike then non-strike - third roll is second fill ball |
| 128 | return True |
| 129 | else: |
| 130 | # First roll was not a strike |
| 131 | if first_roll + second_roll == 10: |
| 132 | # Spare - third roll is fill ball |
| 133 | return True |
| 134 | else: |
| 135 | # Open frame - shouldn't have a third roll |
| 136 | return False |
| 137 | else: |
| 138 | # Too many rolls in 10th frame |
| 139 | return False |
| 140 | |
| 141 | # If we get here, it means we're starting a new frame (frames 1-9) |
| 142 | # This is always valid |
| 143 | return True |
| 144 | |
| 145 | def _is_valid_roll(self): |
| 146 | """ |
| 147 | Validate that the current roll sequence follows bowling rules. |
| 148 | |
| 149 | Returns: |
| 150 | bool: True if the roll sequence is valid, False otherwise |
| 151 | """ |
| 152 | # Check if we're trying to add a roll to a frame that already had a strike (frames 1-9) |
| 153 | frame_count = 0 |
| 154 | roll_index = 0 |
| 155 | |
| 156 | # Process frames 1-9 |
| 157 | while frame_count < 9 and roll_index < len(self.rolls): |
| 158 | if self._is_strike(self.rolls[roll_index]): |
| 159 | # Strike frame - should only have 1 roll |
| 160 | roll_index += 1 |
| 161 | frame_count += 1 |
| 162 | else: |
| 163 | if roll_index + 1 < len(self.rolls): |
| 164 | # Check if two rolls in a frame exceed 10 |
| 165 | if self.rolls[roll_index] + self.rolls[roll_index + 1] > 10: |
| 166 | return False |
| 167 | roll_index += 2 |
| 168 | frame_count += 1 |
| 169 | else: |
| 170 | # Incomplete frame - this is valid |
| 171 | break |
| 172 | |
| 173 | # For the 10th frame, we don't validate the same way since it has special rules |
| 174 | # The 10th frame can have strikes and spares that allow more rolls |
| 175 | return True |
| 176 | |
| 177 | def _is_game_finished(self): |
| 178 | """ |
| 179 | Check if the game is finished according to bowling rules. |
| 180 | |
| 181 | Returns: |
| 182 | bool: True if the game is finished, False otherwise |
| 183 | """ |
| 184 | # Count frames properly considering strikes (1 roll) vs non-strikes (2 rolls) |
| 185 | frame_count = 0 |
| 186 | roll_index = 0 |
| 187 | |
| 188 | # Process frames 1-9 |
| 189 | while frame_count < 9 and roll_index < len(self.rolls): |
| 190 | if self._is_strike(self.rolls[roll_index]): |
| 191 | roll_index += 1 |
| 192 | frame_count += 1 |
| 193 | else: |
| 194 | if roll_index + 1 >= len(self.rolls): |
| 195 | return False # Incomplete frame |
| 196 | roll_index += 2 |
| 197 | frame_count += 1 |
| 198 | |
| 199 | # If we didn't complete 9 frames, game is not finished |
| 200 | if frame_count < 9: |
| 201 | return False |
| 202 | |
| 203 | # Check if we have enough rolls for 10th frame |
| 204 | if roll_index >= len(self.rolls): |
| 205 | return False # No rolls for 10th frame |
| 206 | |
| 207 | # Determine 10th frame requirements |
| 208 | remaining_rolls = len(self.rolls) - roll_index |
| 209 | |
| 210 | if roll_index >= len(self.rolls): |
| 211 | return False |
| 212 | |
| 213 | # First roll of 10th frame |
| 214 | if self._is_strike(self.rolls[roll_index]): |
| 215 | # Strike in 10th frame needs exactly 3 total rolls |
| 216 | return remaining_rolls == 3 |
| 217 | else: |
| 218 | # Need at least 2 rolls for non-strike |
| 219 | if remaining_rolls < 2: |
| 220 | return False |
| 221 | |
| 222 | # Check if first two rolls make a spare |
| 223 | if roll_index + 1 < len(self.rolls) and self.rolls[roll_index] + self.rolls[roll_index + 1] == 10: |
| 224 | # Spare in 10th frame needs exactly 3 total rolls |
| 225 | return remaining_rolls == 3 |
| 226 | else: |
| 227 | # Open frame in 10th frame needs exactly 2 rolls |
| 228 | return remaining_rolls == 2 |
| 7 | 229 | |
| 8 | 230 | def score(self): |
| 9 | | pass |
| 231 | """ |
| 232 | Calculate the total score for the game. |
| 233 | |
| 234 | Returns: |
| 235 | int: The total score of the game |
| 236 | |
| 237 | Raises: |
| 238 | ValueError: If the game is not yet complete |
| 239 | """ |
| 240 | # Edge Case: Game not finished yet |
| 241 | if not self._is_game_finished(): |
| 242 | raise ValueError("Score cannot be taken until the end of the game") |
| 243 | |
| 244 | total_score = 0 |
| 245 | roll_index = 0 |
| 246 | |
| 247 | for frame in range(10): |
| 248 | # Edge Case: Incomplete game (should not happen with proper validation above) |
| 249 | if roll_index >= len(self.rolls): |
| 250 | raise ValueError("Incomplete game") |
| 251 | |
| 252 | if self._is_strike(self.rolls[roll_index]): |
| 253 | # Strike frame |
| 254 | total_score += 10 + self._next_two_balls_score(roll_index) |
| 255 | roll_index += 1 |
| 256 | else: |
| 257 | frame_score = self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 258 | if frame_score == 10: |
| 259 | # Spare frame |
| 260 | total_score += 10 + self.rolls[roll_index + 2] |
| 261 | else: |
| 262 | # Open frame |
| 263 | total_score += frame_score |
| 264 | roll_index += 2 |
| 265 | |
| 266 | return total_score |
| 267 | |
| 268 | def _is_strike(self, pins): |
| 269 | """ |
| 270 | Check if a roll is a strike. |
| 271 | |
| 272 | Args: |
| 273 | pins (int): Number of pins knocked down |
| 274 | |
| 275 | Returns: |
| 276 | bool: True if it's a strike, False otherwise |
| 277 | """ |
| 278 | return pins == 10 |
| 279 | |
| 280 | def _next_two_balls_score(self, roll_index): |
| 281 | """ |
| 282 | Calculate the score of the next two balls after a strike. |
| 283 | |
| 284 | Args: |
| 285 | roll_index (int): Index of the strike roll |
| 286 | |
| 287 | Returns: |
| 288 | int: Sum of the next two rolls |
| 289 | """ |
| 290 | # Edge Case: Not enough rolls after a strike (should not happen with proper validation) |
| 291 | if roll_index + 1 >= len(self.rolls): |
| 292 | return 0 |
| 293 | if roll_index + 2 >= len(self.rolls): |
| 294 | return self.rolls[roll_index + 1] |
| 295 | |
| 296 | # If the next roll is also a strike |
| 297 | if self._is_strike(self.rolls[roll_index + 1]): |
| 298 | return 10 + self.rolls[roll_index + 2] |
| 299 | else: |
| 300 | return self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 301 | |
| 302 | # Handled Edge Cases: Game already finished, invalid number of pins, too many pins in a frame, invalid fill balls, game completion check, incomplete game |