| 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) |
| 20 | if pins < 0: |
| 21 | raise ValueError("invalid roll") |
| 22 | |
| 23 | # Edge Case: Game already has 10 frames and no more rolls are allowed |
| 24 | if self.is_game_complete(): |
| 25 | raise ValueError("Cannot roll after game is over") |
| 26 | |
| 27 | # Edge Case: In tenth frame, trying to roll after a valid frame is completed |
| 28 | if len(self.rolls) >= 18 and self.tenth_frame_complete(): |
| 29 | raise ValueError("Cannot roll after game is over") |
| 30 | |
| 31 | # Edge Case: Validate pin counts for regular frames (1-9) |
| 32 | if len(self.rolls) < 18: |
| 33 | # Check if we're in the second roll of a frame (after a non-strike first roll) |
| 34 | if len(self.rolls) % 2 == 1 and self.rolls[-1] != 10: |
| 35 | # Previous roll was not a strike, so we need to check the sum |
| 36 | if self.rolls[-1] + pins > 10: |
| 37 | raise ValueError("invalid roll") |
| 38 | |
| 39 | # Edge Case: In tenth frame, second roll exceeds remaining pins |
| 40 | if len(self.rolls) >= 18 and len(self.rolls) % 2 == 1 and self.rolls[-1] != 10: |
| 41 | if self.rolls[-1] + pins > 10: |
| 42 | raise ValueError("Pin count exceeds pins on the lane") |
| 43 | |
| 44 | # Edge Case: Validate fill balls in tenth frame |
| 45 | if len(self.rolls) >= 18: |
| 46 | # If first ball of tenth frame was a strike |
| 47 | if len(self.rolls) >= 19 and self.rolls[18] == 10: |
| 48 | # First fill ball (roll 19) |
| 49 | if len(self.rolls) == 19: |
| 50 | if pins > 10: |
| 51 | raise ValueError("invalid fill balls") |
| 52 | # Second fill ball (roll 20) |
| 53 | elif len(self.rolls) == 20: |
| 54 | if self.rolls[19] == 10: |
| 55 | # After a strike, can have up to 10 pins |
| 56 | if pins > 10: |
| 57 | raise ValueError("invalid fill balls") |
| 58 | else: |
| 59 | # After a non-strike, can only have remaining pins |
| 60 | if self.rolls[19] + pins > 10: |
| 61 | raise ValueError("invalid fill balls") |
| 62 | # If tenth frame was a spare |
| 63 | elif len(self.rolls) >= 20 and len(self.rolls) >= 19 and self.rolls[18] + self.rolls[19] == 10: |
| 64 | # Fill ball after spare (roll 20) |
| 65 | if len(self.rolls) == 20: |
| 66 | if pins > 10: |
| 67 | raise ValueError("invalid fill balls") |
| 68 | |
| 69 | # Edge Case: Invalid number of pins (more than 10) for non-fill balls |
| 70 | if pins > 10: |
| 71 | # For fill balls, we've already handled the validation above |
| 72 | # For non-fill balls, this is an error |
| 73 | if len(self.rolls) < 18 or (len(self.rolls) >= 18 and |
| 74 | not (len(self.rolls) == 19 or |
| 75 | (len(self.rolls) == 20 and self.rolls[18] == 10) or |
| 76 | (len(self.rolls) == 20 and self.rolls[18] + self.rolls[19] == 10))): |
| 77 | raise ValueError("Pin count exceeds pins on the lane") |
| 78 | |
| 79 | self.rolls.append(pins) |
| 80 | |
| 8 | 81 | def score(self): |
| 9 | | pass |
| 82 | """ |
| 83 | Calculate the total score for the game. |
| 84 | |
| 85 | Returns: |
| 86 | int: The total score of the game. |
| 87 | |
| 88 | Raises: |
| 89 | ValueError: If the game is not yet complete. |
| 90 | """ |
| 91 | # Edge Case: Game is not complete yet |
| 92 | if not self.is_game_complete(): |
| 93 | raise IndexError("game not complete") |
| 94 | |
| 95 | total_score = 0 |
| 96 | roll_index = 0 |
| 97 | |
| 98 | for frame in range(10): |
| 99 | # Edge Case: Strike in regular frames (1-9) |
| 100 | if self.is_strike(roll_index) and frame < 9: |
| 101 | total_score += 10 + self.strike_bonus(roll_index) |
| 102 | roll_index += 1 |
| 103 | # Edge Case: Spare in regular frames (1-9) |
| 104 | elif self.is_spare(roll_index) and frame < 9: |
| 105 | total_score += 10 + self.spare_bonus(roll_index) |
| 106 | roll_index += 2 |
| 107 | # Edge Case: Open frame in regular frames (1-9) |
| 108 | elif frame < 9: |
| 109 | total_score += self.sum_of_balls_in_frame(roll_index) |
| 110 | roll_index += 2 |
| 111 | # Edge Case: Tenth frame scoring |
| 112 | else: # frame == 9 |
| 113 | # Strike in tenth frame |
| 114 | if self.is_strike(roll_index): |
| 115 | # Edge Case: Tenth frame has only one strike recorded |
| 116 | if len(self.rolls) < roll_index + 3: |
| 117 | raise ValueError("Score cannot be taken until the end of the game") |
| 118 | total_score += 10 + self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 119 | # Spare in tenth frame |
| 120 | elif self.is_spare(roll_index): |
| 121 | # Edge Case: Tenth frame has only one spare recorded |
| 122 | if len(self.rolls) < roll_index + 3: |
| 123 | raise ValueError("Score cannot be taken until the end of the game") |
| 124 | total_score += 10 + self.rolls[roll_index + 2] |
| 125 | # Open frame in tenth frame |
| 126 | else: |
| 127 | # Edge Case: Tenth frame is incomplete |
| 128 | if len(self.rolls) < roll_index + 2: |
| 129 | raise ValueError("Score cannot be taken until the end of the game") |
| 130 | total_score += self.sum_of_balls_in_frame(roll_index) |
| 131 | |
| 132 | return total_score |
| 133 | |
| 134 | def is_game_complete(self): |
| 135 | """ |
| 136 | Check if the game is complete. |
| 137 | |
| 138 | Returns: |
| 139 | bool: True if the game is complete, False otherwise. |
| 140 | """ |
| 141 | # Count frames properly considering strikes |
| 142 | frame_count = 0 |
| 143 | roll_index = 0 |
| 144 | |
| 145 | while roll_index < len(self.rolls) and frame_count < 9: |
| 146 | if self.rolls[roll_index] == 10: # Strike |
| 147 | frame_count += 1 |
| 148 | roll_index += 1 |
| 149 | else: |
| 150 | # Need two rolls for a non-strike frame |
| 151 | if roll_index + 1 < len(self.rolls): |
| 152 | frame_count += 1 |
| 153 | roll_index += 2 |
| 154 | else: |
| 155 | # Incomplete frame |
| 156 | break |
| 157 | |
| 158 | # If we haven't completed 9 frames, game is not complete |
| 159 | if frame_count < 9: |
| 160 | return False |
| 161 | |
| 162 | # Now handle the 10th frame |
| 163 | if roll_index >= len(self.rolls): |
| 164 | return False # Haven't started 10th frame yet |
| 165 | |
| 166 | # Check 10th frame |
| 167 | if self.rolls[roll_index] == 10: # Strike in 10th frame |
| 168 | # Need 2 more rolls |
| 169 | return len(self.rolls) >= roll_index + 3 |
| 170 | elif roll_index + 1 < len(self.rolls): |
| 171 | # Have at least 2 rolls in 10th frame |
| 172 | if self.rolls[roll_index] + self.rolls[roll_index + 1] == 10: # Spare |
| 173 | # Need 1 more roll |
| 174 | return len(self.rolls) >= roll_index + 3 |
| 175 | else: # Open frame |
| 176 | return len(self.rolls) >= roll_index + 2 |
| 177 | else: |
| 178 | # Only 1 roll in 10th frame so far |
| 179 | return False |
| 180 | |
| 181 | def tenth_frame_complete(self): |
| 182 | """ |
| 183 | Check if the tenth frame is complete. |
| 184 | |
| 185 | Returns: |
| 186 | bool: True if the tenth frame is complete, False otherwise. |
| 187 | """ |
| 188 | # Edge Case: Not enough rolls to determine tenth frame status |
| 189 | if len(self.rolls) < 18: |
| 190 | return False |
| 191 | |
| 192 | # If we have exactly 18 rolls, we haven't started the tenth frame |
| 193 | if len(self.rolls) == 18: |
| 194 | return False |
| 195 | |
| 196 | # If first ball was a strike |
| 197 | if len(self.rolls) >= 19 and self.rolls[18] == 10: |
| 198 | # Edge Case: Strike in tenth frame but not enough rolls for fill balls |
| 199 | if len(self.rolls) < 21: |
| 200 | return False |
| 201 | # If second ball made a spare |
| 202 | elif len(self.rolls) >= 20 and self.rolls[18] + self.rolls[19] == 10: |
| 203 | # Edge Case: Spare in tenth frame but not enough rolls for fill ball |
| 204 | if len(self.rolls) < 21: |
| 205 | return False |
| 206 | # If tenth frame is open |
| 207 | elif len(self.rolls) >= 20: |
| 208 | return True |
| 209 | |
| 210 | return False |
| 211 | |
| 212 | def is_strike(self, roll_index): |
| 213 | """ |
| 214 | Check if a roll is a strike. |
| 215 | |
| 216 | Args: |
| 217 | roll_index (int): Index of the roll to check. |
| 218 | |
| 219 | Returns: |
| 220 | bool: True if the roll is a strike, False otherwise. |
| 221 | """ |
| 222 | # Edge Case: Roll index out of bounds |
| 223 | if roll_index >= len(self.rolls): |
| 224 | return False |
| 225 | return self.rolls[roll_index] == 10 |
| 226 | |
| 227 | def is_spare(self, roll_index): |
| 228 | """ |
| 229 | Check if a frame is a spare. |
| 230 | |
| 231 | Args: |
| 232 | roll_index (int): Index of the first roll in the frame. |
| 233 | |
| 234 | Returns: |
| 235 | bool: True if the frame is a spare, False otherwise. |
| 236 | """ |
| 237 | # Edge Case: Not enough rolls to form a frame |
| 238 | if roll_index + 1 >= len(self.rolls): |
| 239 | return False |
| 240 | return self.rolls[roll_index] + self.rolls[roll_index + 1] == 10 |
| 241 | |
| 242 | def strike_bonus(self, roll_index): |
| 243 | """ |
| 244 | Calculate the bonus for a strike. |
| 245 | |
| 246 | Args: |
| 247 | roll_index (int): Index of the strike roll. |
| 248 | |
| 249 | Returns: |
| 250 | int: The bonus points for the strike. |
| 251 | """ |
| 252 | # Edge Case: Not enough rolls for calculating strike bonus |
| 253 | if roll_index + 2 >= len(self.rolls): |
| 254 | return 0 |
| 255 | return self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 256 | |
| 257 | def spare_bonus(self, roll_index): |
| 258 | """ |
| 259 | Calculate the bonus for a spare. |
| 260 | |
| 261 | Args: |
| 262 | roll_index (int): Index of the first roll in the spare frame. |
| 263 | |
| 264 | Returns: |
| 265 | int: The bonus points for the spare. |
| 266 | """ |
| 267 | # Edge Case: Not enough rolls for calculating spare bonus |
| 268 | if roll_index + 2 >= len(self.rolls): |
| 269 | return 0 |
| 270 | return self.rolls[roll_index + 2] |
| 271 | |
| 272 | def sum_of_balls_in_frame(self, roll_index): |
| 273 | """ |
| 274 | Calculate the sum of pins knocked down in a frame. |
| 275 | |
| 276 | Args: |
| 277 | roll_index (int): Index of the first roll in the frame. |
| 278 | |
| 279 | Returns: |
| 280 | int: The sum of pins knocked down in the frame. |
| 281 | """ |
| 282 | # Edge Case: Not enough rolls to form a complete frame |
| 283 | if roll_index + 1 >= len(self.rolls): |
| 284 | return 0 |
| 285 | return self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 286 | |
| 287 | # 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 tenth frame trying to roll after a valid frame is completed, In tenth frame second roll exceeds remaining pins, Game is not complete yet, Strike in regular frames, Spare in regular frames, Open frame in regular frames, Tenth frame scoring, Less than 10 frames played, Exactly 10 frames with no strikes or spares in tenth frame, Tenth frame had a strike or spare check if fill balls are complete, Not enough rolls to determine tenth frame status, Strike in tenth frame but not enough rolls for fill balls, Spare in tenth frame but not enough rolls for fill ball, Roll index out of bounds, Not enough rolls to form a frame, Not enough rolls for calculating strike bonus, Not enough rolls for calculating spare bonus, Not enough rolls to form a complete frame |