| 1 | 1 | class BowlingGame: |
| 2 | 2 | def __init__(self): |
| 3 | | pass |
| 4 | | |
| 3 | """ |
| 4 | Initialize a new bowling game. |
| 5 | """ |
| 6 | self.rolls = [] |
| 7 | self.current_roll = 0 |
| 8 | |
| 5 | 9 | 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 number of pins (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 has 10 frames and no more rolls are allowed |
| 24 | if self.is_game_complete(): |
| 25 | raise ValueError("Game is already complete") |
| 26 | |
| 27 | # Edge Case: In regular frames, check if frame would exceed 10 pins |
| 28 | if len(self.rolls) < 18: # Before 10th frame |
| 29 | # Determine if this is the first or second roll of a frame |
| 30 | frame_start = self._find_frame_start(len(self.rolls)) |
| 31 | if frame_start is not None and frame_start < len(self.rolls): |
| 32 | # This is the second roll of a frame |
| 33 | if self.rolls[frame_start] != 10 and self.rolls[frame_start] + pins > 10: |
| 34 | raise ValueError("Pin count exceeds pins on the lane") |
| 35 | elif len(self.rolls) >= 18: # We're in the 10th frame or fill balls |
| 36 | # If this is the second roll of the 10th frame |
| 37 | if len(self.rolls) == 19: |
| 38 | # If first roll was a strike, any second roll is valid (0-10) |
| 39 | # If first roll was not a strike, sum must be <= 10 |
| 40 | if self.rolls[18] != 10 and self.rolls[18] + pins > 10: |
| 41 | raise ValueError("Pin count exceeds pins on the lane") |
| 42 | # If this is a fill ball (20th or later roll) |
| 43 | elif len(self.rolls) >= 20: |
| 44 | # Check if we're allowed to roll based on 10th frame |
| 45 | if not self.can_roll_fill_ball(): |
| 46 | raise ValueError("Game is already complete") |
| 47 | |
| 48 | self.rolls.append(pins) |
| 49 | |
| 8 | 50 | def score(self): |
| 9 | | pass |
| 51 | """ |
| 52 | Calculate the total score for the game. |
| 53 | |
| 54 | Returns: |
| 55 | int: The total score of the game. |
| 56 | |
| 57 | Raises: |
| 58 | ValueError: If the game is not yet complete. |
| 59 | """ |
| 60 | # Edge Case: Game is not complete |
| 61 | if not self.is_game_complete(): |
| 62 | raise ValueError("Game is not yet complete") |
| 63 | |
| 64 | total_score = 0 |
| 65 | roll_index = 0 |
| 66 | |
| 67 | for frame in range(10): |
| 68 | # Edge Case: Strike (first roll of frame is 10) |
| 69 | if self.is_strike(roll_index): |
| 70 | total_score += 10 + self.strike_bonus(roll_index) |
| 71 | roll_index += 1 |
| 72 | # Edge Case: Spare (sum of two rolls in frame is 10) |
| 73 | elif self.is_spare(roll_index): |
| 74 | total_score += 10 + self.spare_bonus(roll_index) |
| 75 | roll_index += 2 |
| 76 | # Edge Case: Open frame (sum of two rolls < 10) |
| 77 | else: |
| 78 | total_score += self.sum_of_balls_in_frame(roll_index) |
| 79 | roll_index += 2 |
| 80 | |
| 81 | return total_score |
| 82 | |
| 83 | def is_game_complete(self): |
| 84 | """ |
| 85 | Check if the game is complete (10 frames have been played). |
| 86 | |
| 87 | Returns: |
| 88 | bool: True if the game is complete, False otherwise. |
| 89 | """ |
| 90 | # Count frames by processing rolls |
| 91 | frame_count = 0 |
| 92 | roll_index = 0 |
| 93 | |
| 94 | # Count complete frames |
| 95 | while roll_index < len(self.rolls) and frame_count < 10: |
| 96 | # If this is a strike |
| 97 | if roll_index < len(self.rolls) and self.rolls[roll_index] == 10: |
| 98 | frame_count += 1 |
| 99 | roll_index += 1 |
| 100 | else: |
| 101 | # Need two rolls for a non-strike frame |
| 102 | if roll_index + 1 >= len(self.rolls): |
| 103 | # Incomplete frame, game not complete |
| 104 | break |
| 105 | frame_count += 1 |
| 106 | roll_index += 2 |
| 107 | |
| 108 | # If we haven't completed 10 frames, game is not complete |
| 109 | if frame_count < 10: |
| 110 | return False |
| 111 | |
| 112 | # If we have more than 10 frames (shouldn't happen), game is complete |
| 113 | if frame_count > 10: |
| 114 | return True |
| 115 | |
| 116 | # We have exactly 10 frames, check if the 10th frame is complete |
| 117 | # Find the start of the 10th frame |
| 118 | tenth_frame_start = self._find_tenth_frame_start() |
| 119 | if tenth_frame_start is None: |
| 120 | return False |
| 121 | |
| 122 | # If we don't have enough rolls to determine the 10th frame, game not complete |
| 123 | if len(self.rolls) <= tenth_frame_start: |
| 124 | return False |
| 125 | |
| 126 | tenth_frame_first = self.rolls[tenth_frame_start] |
| 127 | |
| 128 | # If 10th frame was a strike, need 2 fill balls (total 3 rolls for 10th frame) |
| 129 | if tenth_frame_first == 10: |
| 130 | return len(self.rolls) >= tenth_frame_start + 3 |
| 131 | # If we don't have second roll of 10th frame yet |
| 132 | elif len(self.rolls) < tenth_frame_start + 2: |
| 133 | return False |
| 134 | # If 10th frame was a spare, need 1 fill ball (total 3 rolls for 10th frame) |
| 135 | elif tenth_frame_first + self.rolls[tenth_frame_start + 1] == 10: |
| 136 | return len(self.rolls) >= tenth_frame_start + 3 |
| 137 | # Open frame, no fill balls needed (total 2 rolls for 10th frame) |
| 138 | else: |
| 139 | return len(self.rolls) >= tenth_frame_start + 2 |
| 140 | |
| 141 | def _find_frame_start(self, roll_position): |
| 142 | """ |
| 143 | Find the starting index of the frame that contains the given roll position. |
| 144 | |
| 145 | Args: |
| 146 | roll_position (int): The position we want to find the frame start for. |
| 147 | |
| 148 | Returns: |
| 149 | int: Index of the first roll of the frame, or None if not found. |
| 150 | """ |
| 151 | frame_count = 0 |
| 152 | roll_index = 0 |
| 153 | |
| 154 | while roll_index < roll_position and frame_count < 10: |
| 155 | if roll_index >= len(self.rolls): |
| 156 | break |
| 157 | |
| 158 | if self.rolls[roll_index] == 10: |
| 159 | frame_count += 1 |
| 160 | roll_index += 1 |
| 161 | else: |
| 162 | if roll_index + 1 >= len(self.rolls): |
| 163 | break |
| 164 | frame_count += 1 |
| 165 | roll_index += 2 |
| 166 | |
| 167 | return roll_index if roll_index < roll_position else None |
| 168 | |
| 169 | def _find_tenth_frame_start(self): |
| 170 | """ |
| 171 | Find the starting index of the 10th frame. |
| 172 | |
| 173 | Returns: |
| 174 | int: Index of the first roll of the 10th frame, or None if not found. |
| 175 | """ |
| 176 | frame_count = 0 |
| 177 | roll_index = 0 |
| 178 | |
| 179 | while roll_index < len(self.rolls) and frame_count < 9: |
| 180 | if self.rolls[roll_index] == 10: |
| 181 | frame_count += 1 |
| 182 | roll_index += 1 |
| 183 | else: |
| 184 | if roll_index + 1 >= len(self.rolls): |
| 185 | return None |
| 186 | frame_count += 1 |
| 187 | roll_index += 2 |
| 188 | |
| 189 | return roll_index if frame_count == 9 else None |
| 190 | |
| 191 | def is_strike(self, roll_index): |
| 192 | """ |
| 193 | Check if the roll at roll_index is a strike. |
| 194 | |
| 195 | Args: |
| 196 | roll_index (int): Index of the roll to check. |
| 197 | |
| 198 | Returns: |
| 199 | bool: True if the roll is a strike, False otherwise. |
| 200 | """ |
| 201 | return self.rolls[roll_index] == 10 |
| 202 | |
| 203 | def is_spare(self, roll_index): |
| 204 | """ |
| 205 | Check if the frame starting at roll_index is a spare. |
| 206 | |
| 207 | Args: |
| 208 | roll_index (int): Index of the first roll of the frame. |
| 209 | |
| 210 | Returns: |
| 211 | bool: True if the frame is a spare, False otherwise. |
| 212 | """ |
| 213 | return self.rolls[roll_index] + self.rolls[roll_index + 1] == 10 |
| 214 | |
| 215 | def sum_of_balls_in_frame(self, roll_index): |
| 216 | """ |
| 217 | Calculate the sum of pins knocked down in a frame. |
| 218 | |
| 219 | Args: |
| 220 | roll_index (int): Index of the first roll of the frame. |
| 221 | |
| 222 | Returns: |
| 223 | int: Sum of pins knocked down in the frame. |
| 224 | """ |
| 225 | return self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 226 | |
| 227 | def spare_bonus(self, roll_index): |
| 228 | """ |
| 229 | Calculate the bonus for a spare. |
| 230 | |
| 231 | Args: |
| 232 | roll_index (int): Index of the first roll of the spare frame. |
| 233 | |
| 234 | Returns: |
| 235 | int: The bonus points for the spare. |
| 236 | """ |
| 237 | return self.rolls[roll_index + 2] |
| 238 | |
| 239 | def strike_bonus(self, roll_index): |
| 240 | """ |
| 241 | Calculate the bonus for a strike. |
| 242 | |
| 243 | Args: |
| 244 | roll_index (int): Index of the strike roll. |
| 245 | |
| 246 | Returns: |
| 247 | int: The bonus points for the strike. |
| 248 | """ |
| 249 | return self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 250 | |
| 251 | def can_roll_fill_ball(self): |
| 252 | """ |
| 253 | Determine if a fill ball can be rolled based on the 10th frame. |
| 254 | |
| 255 | Returns: |
| 256 | bool: True if a fill ball can be rolled, False otherwise. |
| 257 | """ |
| 258 | # Game must have at least 12 rolls to need fill balls |
| 259 | if len(self.rolls) < 12: |
| 260 | return False |
| 261 | |
| 262 | # Find the start of the 10th frame |
| 263 | tenth_frame_start = self._find_tenth_frame_start() |
| 264 | if tenth_frame_start is None: |
| 265 | return False |
| 266 | |
| 267 | # Check if we're in a position to roll fill balls |
| 268 | if len(self.rolls) < tenth_frame_start + 2: |
| 269 | return False |
| 270 | |
| 271 | tenth_frame_first = self.rolls[tenth_frame_start] |
| 272 | |
| 273 | # If 10th frame was a strike, we need two fill balls |
| 274 | if tenth_frame_first == 10: |
| 275 | return len(self.rolls) < tenth_frame_start + 3 |
| 276 | |
| 277 | # If 10th frame was a spare, we need one fill ball |
| 278 | elif (tenth_frame_start + 1 < len(self.rolls) and |
| 279 | tenth_frame_first + self.rolls[tenth_frame_start + 1] == 10): |
| 280 | return len(self.rolls) < tenth_frame_start + 3 |
| 281 | |
| 282 | # If 10th frame was open, no fill balls needed |
| 283 | else: |
| 284 | return False |
| 285 | |
| 286 | # Handled Edge Cases: Invalid number of pins (negative), Invalid number of pins (more than 10), Game already has 10 frames and no more rolls are allowed, In last frame check if roll is valid, In regular frames check if frame would exceed 10 pins |
| 287 | # Handled Edge Cases: Game is not complete, Not enough rolls, Exactly 9 frames but 9th frame is a strike, 10 frames but need fill balls, 10 frames with fill balls |