| 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("Negative roll is invalid") |
| 22 | |
| 23 | # Edge Case: Invalid number of pins (more than 10 in a single roll) |
| 24 | if pins > 10: |
| 25 | raise ValueError("Pin count exceeds pins on the lane") |
| 26 | |
| 27 | # Edge Case: Can't roll after game is over |
| 28 | if self.is_game_over(): |
| 29 | raise IndexError("Cannot roll after game is over") |
| 30 | |
| 31 | # Handle 10th frame pin validation |
| 32 | if len(self.rolls) >= 18: # In 10th frame |
| 33 | # In 10th frame, each roll can knock down 0-10 pins regardless of previous rolls |
| 34 | # The only constraint is that we can't have more than 21 rolls total |
| 35 | if len(self.rolls) >= 21: |
| 36 | raise IndexError("Cannot roll after game is over") |
| 37 | # Special validation for fill balls |
| 38 | if len(self.rolls) == 20: # Third roll in 10th frame (first fill ball) |
| 39 | # If first two rolls were a strike and a non-strike, validate the sum |
| 40 | if self.rolls[18] == 10 and self.rolls[19] != 10: |
| 41 | if self.rolls[19] + pins > 10: |
| 42 | raise ValueError("Pin count exceeds pins on the lane") |
| 43 | # If first two rolls were not a strike but made a spare, any value is valid |
| 44 | elif self.rolls[18] != 10 and self.rolls[18] + self.rolls[19] == 10: |
| 45 | pass # Any value is valid for a spare's fill ball |
| 46 | else: # In regular frames (1-9) |
| 47 | # Edge Case: In first roll of frame, can't knock down more than 10 pins |
| 48 | if len(self.rolls) % 2 == 0: # First roll of frame (0, 2, 4, ...) |
| 49 | if pins > 10: |
| 50 | raise ValueError("Pin count exceeds pins on the lane") |
| 51 | else: # Second roll of frame |
| 52 | # No validation needed here - validation happens during scoring |
| 53 | |
| 54 | self.rolls.append(pins) |
| 55 | self.current_roll += 1 |
| 56 | |
| 8 | 57 | def score(self): |
| 9 | | pass |
| 58 | """ |
| 59 | Calculate the total score for the game. |
| 60 | |
| 61 | Returns: |
| 62 | int: The total score for the game. |
| 63 | |
| 64 | Raises: |
| 65 | IndexError: If the game is not yet complete. |
| 66 | """ |
| 67 | # Edge Case: Game is not complete |
| 68 | if not self.is_game_over(): |
| 69 | raise IndexError("Score cannot be taken until the end of the game") |
| 70 | |
| 71 | # Edge Case: Invalid game state (too many rolls) |
| 72 | if len(self.rolls) > 21: |
| 73 | raise ValueError("Cannot score an incomplete game") |
| 74 | |
| 75 | total_score = 0 |
| 76 | roll_index = 0 |
| 77 | |
| 78 | for frame in range(10): |
| 79 | # Edge Case: Incomplete game |
| 80 | if roll_index >= len(self.rolls): |
| 81 | raise ValueError("Cannot score an incomplete game") |
| 82 | |
| 83 | if self.is_strike(roll_index): # Strike |
| 84 | # Edge Case: Not enough rolls to calculate strike bonus |
| 85 | if roll_index + 2 >= len(self.rolls): |
| 86 | raise ValueError("Cannot score an incomplete game") |
| 87 | total_score += 10 + self.strike_bonus(roll_index) |
| 88 | roll_index += 1 |
| 89 | elif self.is_spare(roll_index): # Spare |
| 90 | # Edge Case: Not enough rolls to calculate spare bonus |
| 91 | if roll_index + 2 >= len(self.rolls): |
| 92 | raise ValueError("Cannot score an incomplete game") |
| 93 | total_score += 10 + self.spare_bonus(roll_index) |
| 94 | roll_index += 2 |
| 95 | else: # Open frame |
| 96 | # Edge Case: Not enough rolls for open frame |
| 97 | if roll_index + 1 >= len(self.rolls): |
| 98 | raise ValueError("Cannot score an incomplete game") |
| 99 | total_score += self.sum_of_balls_in_frame(roll_index) |
| 100 | roll_index += 2 |
| 101 | |
| 102 | return total_score |
| 103 | |
| 104 | def is_game_over(self): |
| 105 | """ |
| 106 | Check if the game is over. |
| 107 | |
| 108 | Returns: |
| 109 | bool: True if the game is over, False otherwise. |
| 110 | """ |
| 111 | # Game cannot be over with fewer than 18 rolls (9 frames) |
| 112 | if len(self.rolls) < 18: |
| 113 | return False |
| 114 | |
| 115 | # Maximum possible rolls is 21 (9 strikes + 3 strikes in 10th frame) |
| 116 | if len(self.rolls) > 21: |
| 117 | return True |
| 118 | |
| 119 | # After 18 rolls, we've completed 9 frames |
| 120 | # Check 10th frame conditions |
| 121 | if len(self.rolls) == 18: |
| 122 | # If we have exactly 18 rolls, we've completed 9 frames |
| 123 | # and have made the first roll of the 10th frame |
| 124 | # We need at least one more roll to complete the 10th frame |
| 125 | return False |
| 126 | |
| 127 | if len(self.rolls) == 19: |
| 128 | # We have 19 rolls - completed 9 frames and made 1 roll in 10th frame |
| 129 | # We need at least one more roll to complete the 10th frame |
| 130 | return False |
| 131 | |
| 132 | if len(self.rolls) == 20: |
| 133 | # We have 20 rolls - completed 9 frames and made 2 rolls in 10th frame |
| 134 | # If the first roll of the 10th frame was a strike, we need one more bonus roll |
| 135 | if self.rolls[18] == 10: |
| 136 | return False |
| 137 | # If the first two rolls of the 10th frame make a spare, we need one more bonus roll |
| 138 | elif self.rolls[18] + self.rolls[19] == 10: |
| 139 | return False |
| 140 | else: # Open frame |
| 141 | return True # Game over |
| 142 | |
| 143 | if len(self.rolls) >= 21: |
| 144 | # We have all possible rolls (21) |
| 145 | # Game is over |
| 146 | return True |
| 147 | |
| 148 | return False |
| 149 | |
| 150 | def is_strike(self, roll_index): |
| 151 | """ |
| 152 | Check if a roll is a strike. |
| 153 | |
| 154 | Args: |
| 155 | roll_index (int): Index of the roll to check. |
| 156 | |
| 157 | Returns: |
| 158 | bool: True if the roll is a strike, False otherwise. |
| 159 | """ |
| 160 | return self.rolls[roll_index] == 10 |
| 161 | |
| 162 | def is_spare(self, roll_index): |
| 163 | """ |
| 164 | Check if a frame is a spare. |
| 165 | |
| 166 | Args: |
| 167 | roll_index (int): Index of the first roll of the frame. |
| 168 | |
| 169 | Returns: |
| 170 | bool: True if the frame is a spare, False otherwise. |
| 171 | """ |
| 172 | return self.rolls[roll_index] + self.rolls[roll_index + 1] == 10 |
| 173 | |
| 174 | def strike_bonus(self, roll_index): |
| 175 | """ |
| 176 | Calculate the bonus for a strike. |
| 177 | |
| 178 | Args: |
| 179 | roll_index (int): Index of the strike roll. |
| 180 | |
| 181 | Returns: |
| 182 | int: The bonus points for the strike. |
| 183 | """ |
| 184 | return self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 185 | |
| 186 | def spare_bonus(self, roll_index): |
| 187 | """ |
| 188 | Calculate the bonus for a spare. |
| 189 | |
| 190 | Args: |
| 191 | roll_index (int): Index of the first roll of the spare frame. |
| 192 | |
| 193 | Returns: |
| 194 | int: The bonus points for the spare. |
| 195 | """ |
| 196 | return self.rolls[roll_index + 2] |
| 197 | |
| 198 | def sum_of_balls_in_frame(self, roll_index): |
| 199 | """ |
| 200 | Calculate the sum of pins knocked down in a frame. |
| 201 | |
| 202 | Args: |
| 203 | roll_index (int): Index of the first roll of the frame. |
| 204 | |
| 205 | Returns: |
| 206 | int: The sum of pins knocked down in the frame. |
| 207 | """ |
| 208 | return self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 209 | |
| 210 | # Handled Edge Cases: Invalid number of pins (negative), Invalid number of pins (more than 10), Can't roll after game is over, In first roll of frame can't knock down more than 10 pins, In second roll can't knock down more than remaining pins, Special handling for 10th frame, Game is not complete, Invalid game state (too many rolls), Not enough rolls to calculate strike bonus, Not enough rolls to calculate spare bonus, Not enough rolls for open frame, Game is over if first roll wasn't a strike and didn't make a spare, Game is over if second roll didn't make a spare with first |