| 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: Negative pins |
| 20 | if pins < 0: |
| 21 | raise ValueError("Pins must be between 0 and 10") |
| 22 | |
| 23 | # Edge Case: Too many pins in a single roll |
| 24 | if pins > 10: |
| 25 | raise ValueError("Pins must be between 0 and 10") |
| 26 | |
| 27 | # Edge Case: Too many pins in a frame |
| 28 | # Only validate frame totals for frames 1-9 |
| 29 | if self.current_roll > 0 and self.rolls[-1] != 10: |
| 30 | # Check if we're in frames 1-9 by counting completed frames |
| 31 | completed_frames = 0 |
| 32 | i = 0 |
| 33 | while i < len(self.rolls) and completed_frames < 9: |
| 34 | if self.rolls[i] == 10: # Strike |
| 35 | completed_frames += 1 |
| 36 | i += 1 |
| 37 | else: |
| 38 | if i + 1 < len(self.rolls): # Make sure we have two rolls |
| 39 | completed_frames += 1 |
| 40 | i += 2 |
| 41 | else: |
| 42 | break |
| 43 | |
| 44 | # Only apply frame validation if we are in frames 1-9 |
| 45 | if completed_frames < 9 or (completed_frames == 9 and len(self.rolls) % 2 == 1): |
| 46 | if self.rolls[-1] + pins > 10: |
| 47 | raise ValueError("Cannot knock down more than 10 pins in a frame") |
| 48 | |
| 49 | # Edge Case: Game already completed |
| 50 | if self.is_game_complete(): |
| 51 | raise ValueError("Game has already ended") |
| 52 | |
| 53 | self.rolls.append(pins) |
| 54 | self.current_roll += 1 |
| 55 | |
| 8 | 56 | def score(self): |
| 9 | | pass |
| 57 | """ |
| 58 | Calculate the total score for the game. |
| 59 | |
| 60 | Returns: |
| 61 | int: The total score for the game. |
| 62 | |
| 63 | Raises: |
| 64 | ValueError: If the game is not yet complete. |
| 65 | """ |
| 66 | # Edge Case: Game not yet complete |
| 67 | if not self.is_game_complete(): |
| 68 | raise ValueError("Game is not yet complete") |
| 69 | |
| 70 | total_score = 0 |
| 71 | roll_index = 0 |
| 72 | |
| 73 | for frame in range(10): |
| 74 | # Edge Case: Strike in regular frames (1-9) |
| 75 | if self.is_strike(roll_index) and frame < 9: |
| 76 | total_score += 10 + self.strike_bonus(roll_index) |
| 77 | roll_index += 1 |
| 78 | # Edge Case: Spare in regular frames (1-9) |
| 79 | elif self.is_spare(roll_index) and frame < 9: |
| 80 | total_score += 10 + self.spare_bonus(roll_index) |
| 81 | roll_index += 2 |
| 82 | # Edge Case: Open frame in regular frames (1-9) |
| 83 | elif frame < 9: |
| 84 | total_score += self.sum_of_balls_in_frame(roll_index) |
| 85 | roll_index += 2 |
| 86 | # Edge Case: Tenth frame handling |
| 87 | else: # 10th frame |
| 88 | # Edge Case: Strike in tenth frame |
| 89 | if self.is_strike(roll_index): |
| 90 | # Edge Case: Invalid fill balls for strike in tenth frame |
| 91 | if len(self.rolls) < roll_index + 3: |
| 92 | raise ValueError("invalid fill balls") |
| 93 | # Edge Case: Too many pins in fill balls |
| 94 | if self.rolls[roll_index+1] != 10 and self.rolls[roll_index+1] + self.rolls[roll_index+2] > 10 and self.rolls[roll_index+1] != 10: |
| 95 | raise ValueError("invalid fill balls") |
| 96 | total_score += 10 + self.rolls[roll_index+1] + self.rolls[roll_index+2] |
| 97 | # Edge Case: Spare in tenth frame |
| 98 | elif self.is_spare(roll_index): |
| 99 | # Edge Case: Invalid fill balls for spare in tenth frame |
| 100 | if len(self.rolls) < roll_index + 3: |
| 101 | raise ValueError("invalid fill balls") |
| 102 | total_score += 10 + self.rolls[roll_index+2] |
| 103 | # Edge Case: Open tenth frame |
| 104 | else: |
| 105 | # Edge Case: Too many rolls in tenth frame |
| 106 | if len(self.rolls) > roll_index + 2: |
| 107 | raise ValueError("invalid fill balls") |
| 108 | total_score += self.sum_of_balls_in_frame(roll_index) |
| 109 | |
| 110 | return total_score |
| 111 | |
| 112 | def is_game_complete(self): |
| 113 | """ |
| 114 | Check if the game is complete. |
| 115 | |
| 116 | Returns: |
| 117 | bool: True if the game is complete, False otherwise. |
| 118 | """ |
| 119 | roll_index = 0 |
| 120 | |
| 121 | for frame in range(10): |
| 122 | # Check if we have enough rolls for this frame |
| 123 | if roll_index >= len(self.rolls): |
| 124 | return False |
| 125 | |
| 126 | if frame < 9: # Frames 1-9 |
| 127 | if roll_index < len(self.rolls) and self.rolls[roll_index] == 10: |
| 128 | roll_index += 1 |
| 129 | else: |
| 130 | # Need 2 rolls for non-strike frames |
| 131 | if roll_index + 1 >= len(self.rolls): |
| 132 | return False |
| 133 | roll_index += 2 |
| 134 | else: # 10th frame |
| 135 | if roll_index >= len(self.rolls): |
| 136 | return False |
| 137 | |
| 138 | if self.rolls[roll_index] == 10: |
| 139 | # Strike in 10th frame needs 2 more rolls |
| 140 | return len(self.rolls) >= roll_index + 3 |
| 141 | elif roll_index + 1 < len(self.rolls) and self.rolls[roll_index] + self.rolls[roll_index + 1] == 10: |
| 142 | # Spare in 10th frame needs 1 more roll |
| 143 | return len(self.rolls) >= roll_index + 3 |
| 144 | else: |
| 145 | # Open frame in 10th frame needs no more rolls |
| 146 | return len(self.rolls) >= roll_index + 2 |
| 147 | |
| 148 | return len(self.rolls) >= roll_index |
| 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 | if roll_index >= len(self.rolls): |
| 161 | return False |
| 162 | return self.rolls[roll_index] == 10 |
| 163 | |
| 164 | def is_spare(self, roll_index): |
| 165 | """ |
| 166 | Check if a frame is a spare. |
| 167 | |
| 168 | Args: |
| 169 | roll_index (int): Index of the first roll in the frame. |
| 170 | |
| 171 | Returns: |
| 172 | bool: True if the frame is a spare, False otherwise. |
| 173 | """ |
| 174 | if roll_index + 1 >= len(self.rolls): |
| 175 | return False |
| 176 | return self.rolls[roll_index] + self.rolls[roll_index + 1] == 10 |
| 177 | |
| 178 | def strike_bonus(self, roll_index): |
| 179 | """ |
| 180 | Calculate the bonus for a strike. |
| 181 | |
| 182 | Args: |
| 183 | roll_index (int): Index of the strike roll. |
| 184 | |
| 185 | Returns: |
| 186 | int: The bonus points for the strike. |
| 187 | """ |
| 188 | return self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 189 | |
| 190 | def spare_bonus(self, roll_index): |
| 191 | """ |
| 192 | Calculate the bonus for a spare. |
| 193 | |
| 194 | Args: |
| 195 | roll_index (int): Index of the first roll in the spare frame. |
| 196 | |
| 197 | Returns: |
| 198 | int: The bonus points for the spare. |
| 199 | """ |
| 200 | return self.rolls[roll_index + 2] |
| 201 | |
| 202 | def sum_of_balls_in_frame(self, roll_index): |
| 203 | """ |
| 204 | Calculate the sum of pins knocked down in a frame. |
| 205 | |
| 206 | Args: |
| 207 | roll_index (int): Index of the first roll in the frame. |
| 208 | |
| 209 | Returns: |
| 210 | int: The sum of pins knocked down in the frame. |
| 211 | """ |
| 212 | return self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 213 | |
| 214 | # Handled Edge Cases: Negative pins, too many pins in a single roll, too many pins in a frame, game already completed, game not yet complete, strike in regular frames, spare in regular frames, open frame in regular frames, tenth frame handling, strike in tenth frame, invalid fill balls for strike in tenth frame, too many pins in fill balls, spare in tenth frame, invalid fill balls for spare in tenth frame, open tenth frame, too many rolls in tenth frame |