| 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 is already over (10 frames completed) |
| 24 | if self.is_game_over(): |
| 25 | raise ValueError("Game is already finished") |
| 26 | |
| 27 | # Edge Case: Invalid frame - sum of pins in a frame exceeds 10 (except for strikes) |
| 28 | if (len(self.rolls) % 2 == 1 and not self.is_strike(len(self.rolls) - 1) and |
| 29 | self.rolls[-1] + pins > 10): |
| 30 | raise ValueError("Cannot knock down more pins than remaining") |
| 31 | |
| 32 | self.rolls.append(pins) |
| 33 | |
| 8 | 34 | def score(self): |
| 9 | | pass |
| 35 | """ |
| 36 | Calculate the total score for the game. |
| 37 | |
| 38 | Returns: |
| 39 | int: The total score of the game. |
| 40 | |
| 41 | Raises: |
| 42 | ValueError: If the game is not yet complete. |
| 43 | """ |
| 44 | # Edge Case: Game is not complete yet |
| 45 | if not self.is_game_over(): |
| 46 | raise ValueError("game not complete") |
| 47 | |
| 48 | score = 0 |
| 49 | roll_index = 0 |
| 50 | |
| 51 | for frame in range(10): |
| 52 | # Edge Case: Strike in regular frames (1-9) |
| 53 | if self.is_strike(roll_index) and frame < 9: |
| 54 | score += 10 + self.strike_bonus(roll_index) |
| 55 | roll_index += 1 |
| 56 | # Edge Case: Spare in regular frames (1-9) |
| 57 | elif self.is_spare(roll_index) and frame < 9: |
| 58 | score += 10 + self.spare_bonus(roll_index) |
| 59 | roll_index += 2 |
| 60 | # Edge Case: Open frame in regular frames (1-9) |
| 61 | elif frame < 9: |
| 62 | score += self.sum_of_balls_in_frame(roll_index) |
| 63 | roll_index += 2 |
| 64 | # Edge Case: Tenth frame handling |
| 65 | else: # 10th frame |
| 66 | # Edge Case: Strike in 10th frame |
| 67 | if self.is_strike(roll_index): |
| 68 | # Edge Case: Second strike in 10th frame |
| 69 | if self.is_strike(roll_index + 1): |
| 70 | # Edge Case: Third strike in 10th frame |
| 71 | score += 10 + 10 + self.rolls[roll_index + 2] |
| 72 | else: |
| 73 | score += 10 + self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 74 | # Edge Case: Spare in 10th frame |
| 75 | elif self.is_spare(roll_index): |
| 76 | score += 10 + self.rolls[roll_index + 2] |
| 77 | # Edge Case: Open 10th frame |
| 78 | else: |
| 79 | score += self.sum_of_balls_in_frame(roll_index) |
| 80 | break |
| 81 | |
| 82 | return score |
| 83 | |
| 84 | def is_game_over(self): |
| 85 | """ |
| 86 | Check if the game is over (10 frames have been played). |
| 87 | |
| 88 | Returns: |
| 89 | bool: True if the game is over, False otherwise. |
| 90 | """ |
| 91 | if len(self.rolls) < 12: # Minimum rolls needed (9 open frames + 3 rolls in 10th) |
| 92 | return False |
| 93 | |
| 94 | roll_index = 0 |
| 95 | frame_count = 0 |
| 96 | |
| 97 | # Count complete frames |
| 98 | while frame_count < 10 and roll_index < len(self.rolls): |
| 99 | frame_count += 1 |
| 100 | |
| 101 | # Edge Case: Strike in regular frames |
| 102 | if frame_count < 10 and self.is_strike(roll_index): |
| 103 | roll_index += 1 |
| 104 | # Edge Case: Any frame in 10th position |
| 105 | elif frame_count == 10: |
| 106 | # Edge Case: Strike in 10th frame |
| 107 | if self.is_strike(roll_index): |
| 108 | # Need two more rolls |
| 109 | if roll_index + 2 < len(self.rolls): |
| 110 | return True |
| 111 | else: |
| 112 | return False |
| 113 | # Edge Case: Spare in 10th frame |
| 114 | elif roll_index + 1 < len(self.rolls) and self.is_spare(roll_index): |
| 115 | # Need one more roll |
| 116 | if roll_index + 2 < len(self.rolls): |
| 117 | return True |
| 118 | else: |
| 119 | return False |
| 120 | # Edge Case: Open 10th frame |
| 121 | else: |
| 122 | # Need two rolls |
| 123 | if roll_index + 1 < len(self.rolls): |
| 124 | return True |
| 125 | else: |
| 126 | return False |
| 127 | else: |
| 128 | roll_index += 2 |
| 129 | |
| 130 | return frame_count >= 10 |
| 131 | |
| 132 | def is_strike(self, roll_index): |
| 133 | """ |
| 134 | Check if a roll is a strike. |
| 135 | |
| 136 | Args: |
| 137 | roll_index (int): Index of the roll to check. |
| 138 | |
| 139 | Returns: |
| 140 | bool: True if the roll is a strike, False otherwise. |
| 141 | """ |
| 142 | # Edge Case: Index out of bounds |
| 143 | if roll_index >= len(self.rolls): |
| 144 | return False |
| 145 | return self.rolls[roll_index] == 10 |
| 146 | |
| 147 | def is_spare(self, roll_index): |
| 148 | """ |
| 149 | Check if two rolls form a spare. |
| 150 | |
| 151 | Args: |
| 152 | roll_index (int): Index of the first roll to check. |
| 153 | |
| 154 | Returns: |
| 155 | bool: True if the two rolls form a spare, False otherwise. |
| 156 | """ |
| 157 | # Edge Case: Index out of bounds |
| 158 | if roll_index + 1 >= len(self.rolls): |
| 159 | return False |
| 160 | return self.rolls[roll_index] + self.rolls[roll_index + 1] == 10 |
| 161 | |
| 162 | def strike_bonus(self, roll_index): |
| 163 | """ |
| 164 | Calculate the bonus for a strike. |
| 165 | |
| 166 | Args: |
| 167 | roll_index (int): Index of the strike roll. |
| 168 | |
| 169 | Returns: |
| 170 | int: The bonus points for the strike. |
| 171 | """ |
| 172 | # Edge Case: Not enough rolls for bonus calculation |
| 173 | if roll_index + 2 >= len(self.rolls): |
| 174 | return 0 |
| 175 | return self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 176 | |
| 177 | def spare_bonus(self, roll_index): |
| 178 | """ |
| 179 | Calculate the bonus for a spare. |
| 180 | |
| 181 | Args: |
| 182 | roll_index (int): Index of the first roll in the spare. |
| 183 | |
| 184 | Returns: |
| 185 | int: The bonus points for the spare. |
| 186 | """ |
| 187 | # Edge Case: Not enough rolls for bonus calculation |
| 188 | if roll_index + 2 >= len(self.rolls): |
| 189 | return 0 |
| 190 | return self.rolls[roll_index + 2] |
| 191 | |
| 192 | def sum_of_balls_in_frame(self, roll_index): |
| 193 | """ |
| 194 | Calculate the sum of pins knocked down in a frame. |
| 195 | |
| 196 | Args: |
| 197 | roll_index (int): Index of the first roll in the frame. |
| 198 | |
| 199 | Returns: |
| 200 | int: The sum of pins knocked down in the frame. |
| 201 | """ |
| 202 | # Edge Case: Not enough rolls in frame |
| 203 | if roll_index + 1 >= len(self.rolls): |
| 204 | return self.rolls[roll_index] if roll_index < len(self.rolls) else 0 |
| 205 | return self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 206 | |
| 207 | # Handled Edge Cases: Invalid number of pins, game already over, game not complete, strike in regular frames, spare in regular frames, open frame in regular frames, strike in 10th frame, second strike in 10th frame, third strike in 10th frame, spare in 10th frame, open 10th frame, index out of bounds, not enough rolls for bonus calculation, not enough rolls in frame |