| 1 | 1 | class BowlingGame: |
| 2 | 2 | def __init__(self): |
| 3 | | pass |
| 3 | self.rolls = [] |
| 4 | self.current_frame = 1 |
| 5 | self.frame_rolls = [] |
| 6 | self.frames_completed = 0 |
| 7 | self.game_over = False |
| 4 | 8 | |
| 5 | 9 | def roll(self, pins): |
| 6 | | pass |
| 7 | | |
| 10 | # Edge Case: Game is already over |
| 11 | if self.game_over: |
| 12 | raise ValueError("Cannot roll after game is over") |
| 13 | |
| 14 | # Edge Case: Invalid pin count (negative or over 10) |
| 15 | if pins < 0 or pins > 10: |
| 16 | raise ValueError("Pins must be between 0 and 10") |
| 17 | |
| 18 | # Edge Case: Too many pins in a frame (except in fill balls) |
| 19 | if not self._is_fill_ball(): |
| 20 | if len(self.frame_rolls) == 1 and self.frame_rolls[0] != 10: |
| 21 | if self.frame_rolls[0] + pins > 10: |
| 22 | raise ValueError("Invalid roll: pins downed exceed 10 in a frame") |
| 23 | |
| 24 | self.rolls.append(pins) |
| 25 | self.frame_rolls.append(pins) |
| 26 | |
| 27 | # Check if frame is completed |
| 28 | frame_completed = False |
| 29 | if self.current_frame < 10: |
| 30 | # Regular frames |
| 31 | if self.frame_rolls[0] == 10 or len(self.frame_rolls) == 2: |
| 32 | frame_completed = True |
| 33 | else: |
| 34 | # 10th frame |
| 35 | if len(self.frame_rolls) == 3: |
| 36 | frame_completed = True |
| 37 | elif len(self.frame_rolls) == 2: |
| 38 | # If not a strike or spare, frame is complete |
| 39 | if self.frame_rolls[0] != 10 and sum(self.frame_rolls) < 10: |
| 40 | frame_completed = True |
| 41 | # If strike, need 2 more rolls |
| 42 | # If spare, need 1 more roll |
| 43 | |
| 44 | if frame_completed: |
| 45 | self.frames_completed += 1 |
| 46 | self.frame_rolls = [] |
| 47 | if self.current_frame < 10: |
| 48 | self.current_frame += 1 |
| 49 | # For 10th frame, check if game is over based on rolls |
| 50 | if self.current_frame == 10: |
| 51 | # Game is over when we have the right number of rolls in 10th frame |
| 52 | if len(self.rolls) >= 20: # At least 20 rolls (2 per frame minimum) |
| 53 | last_frame_rolls = self.rolls[18:] # Get 10th frame rolls |
| 54 | if len(last_frame_rolls) == 2: |
| 55 | # If no strike or spare, game is over |
| 56 | if last_frame_rolls[0] != 10 and sum(last_frame_rolls) < 10: |
| 57 | self.game_over = True |
| 58 | elif len(last_frame_rolls) == 3: |
| 59 | # Game is over after 3 rolls in 10th frame |
| 60 | self.game_over = True |
| 61 | if len(last_frame_rolls) == 2: |
| 62 | # If no strike or spare, game is over |
| 63 | if last_frame_rolls[0] != 10 and sum(last_frame_rolls) < 10: |
| 64 | self.game_over = True |
| 65 | elif len(last_frame_rolls) == 3: |
| 66 | # Game is over after 3 rolls in 10th frame |
| 67 | self.game_over = True |
| 68 | |
| 69 | def _is_fill_ball(self): |
| 70 | # Check if we're in the fill balls for the 10th frame |
| 71 | return self.current_frame > 10 |
| 72 | |
| 73 | def _is_strike(self, frame_index): |
| 74 | return self.frames[frame_index][0] == 10 |
| 75 | |
| 76 | def _is_spare(self, frame_index): |
| 77 | return len(self.frames[frame_index]) == 2 and sum(self.frames[frame_index]) == 10 |
| 78 | |
| 79 | def _get_frame_rolls(self): |
| 80 | # Group rolls into frames |
| 81 | frames = [] |
| 82 | roll_index = 0 |
| 83 | for frame_num in range(10): |
| 84 | if roll_index >= len(self.rolls): |
| 85 | break |
| 86 | frame = [] |
| 87 | # For 10th frame, handle up to 3 rolls |
| 88 | if frame_num == 9: |
| 89 | # Handle 10th frame properly |
| 90 | if roll_index < len(self.rolls): |
| 91 | frame.append(self.rolls[roll_index]) |
| 92 | roll_index += 1 |
| 93 | |
| 94 | # Check if we need more rolls |
| 95 | if roll_index < len(self.rolls): |
| 96 | if frame[0] == 10: # Strike in 10th frame |
| 97 | # Need 2 more rolls |
| 98 | frame.append(self.rolls[roll_index]) |
| 99 | roll_index += 1 |
| 100 | if roll_index < len(self.rolls): |
| 101 | frame.append(self.rolls[roll_index]) |
| 102 | roll_index += 1 |
| 103 | else: |
| 104 | # Not a strike, add second roll |
| 105 | frame.append(self.rolls[roll_index]) |
| 106 | roll_index += 1 |
| 107 | |
| 108 | # Check if it's a spare and needs a third roll |
| 109 | if roll_index < len(self.rolls) and sum(frame[:2]) == 10: |
| 110 | frame.append(self.rolls[roll_index]) |
| 111 | roll_index += 1 |
| 112 | else: |
| 113 | # Regular frames |
| 114 | if self.rolls[roll_index] == 10: # Strike |
| 115 | frame.append(10) |
| 116 | roll_index += 1 |
| 117 | else: |
| 118 | frame.append(self.rolls[roll_index]) |
| 119 | roll_index += 1 |
| 120 | if roll_index < len(self.rolls): |
| 121 | frame.append(self.rolls[roll_index]) |
| 122 | roll_index += 1 |
| 123 | frames.append(frame) |
| 124 | return frames |
| 125 | |
| 8 | 126 | def score(self): |
| 9 | | pass |
| 127 | # Edge Case: Game not completed |
| 128 | if self.frames_completed < 10: |
| 129 | raise ValueError("Game not yet complete") |
| 130 | |
| 131 | frames = self._get_frame_rolls() |
| 132 | total_score = 0 |
| 133 | roll_index = 0 |
| 134 | |
| 135 | for frame_num in range(10): |
| 136 | frame = frames[frame_num] |
| 137 | |
| 138 | # Edge Case: Incomplete frame data |
| 139 | if len(frame) == 0: |
| 140 | raise ValueError("Incomplete game") |
| 141 | |
| 142 | if frame_num < 9: # Frames 1-9 |
| 143 | if len(frame) == 1 and frame[0] == 10: # Strike |
| 144 | # Edge Case: Not enough rolls for strike calculation |
| 145 | if roll_index + 2 >= len(self.rolls): |
| 146 | raise ValueError("Incomplete game") |
| 147 | total_score += 10 + self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 148 | roll_index += 1 |
| 149 | elif len(frame) == 2 and sum(frame) == 10: # Spare |
| 150 | # Edge Case: Not enough rolls for spare calculation |
| 151 | if roll_index + 2 >= len(self.rolls): |
| 152 | raise ValueError("Incomplete game") |
| 153 | total_score += 10 + self.rolls[roll_index + 2] |
| 154 | roll_index += 2 |
| 155 | else: # Open frame |
| 156 | total_score += sum(frame) |
| 157 | roll_index += 2 |
| 158 | else: # 10th frame |
| 159 | # Edge Case: Invalid 10th frame |
| 160 | if len(frame) < 2: |
| 161 | raise ValueError("Invalid 10th frame") |
| 162 | |
| 163 | total_score += sum(frame) |
| 164 | |
| 165 | # Validate 10th frame rules |
| 166 | if len(frame) == 2: |
| 167 | # If not a strike or spare, that's fine |
| 168 | if frame[0] != 10 and sum(frame) < 10: |
| 169 | pass |
| 170 | # If it's a spare, make sure there's a third roll |
| 171 | elif frame[0] != 10 and sum(frame) == 10: |
| 172 | pass |
| 173 | # If first roll is strike, need two more rolls |
| 174 | elif frame[0] == 10 and len(frame) < 3: |
| 175 | raise ValueError("Invalid 10th frame") |
| 176 | elif len(frame) == 3: |
| 177 | # Validate the fill balls - only if we actually have fill balls |
| 178 | # Fill balls only occur when first roll is strike OR first two rolls are spare |
| 179 | if frame[0] == 10: # First roll was a strike |
| 180 | # Second roll can be 0-10, third roll depends on second |
| 181 | if frame[1] == 10: # Second roll also strike |
| 182 | # Third roll can be 0-10 |
| 183 | if frame[2] < 0 or frame[2] > 10: |
| 184 | raise ValueError("invalid fill balls") |
| 185 | else: # Second roll not strike |
| 186 | if frame[1] + frame[2] > 10: |
| 187 | raise ValueError("invalid fill balls") |
| 188 | elif frame[0] + frame[1] == 10: # First two rolls were a spare |
| 189 | # Third roll can be 0-10 |
| 190 | if frame[2] < 0 or frame[2] > 10: |
| 191 | raise ValueError("invalid fill balls") |
| 192 | # If neither strike nor spare, then we shouldn't have 3 rolls |
| 193 | # This would be an invalid game state |
| 194 | elif frame[0] + frame[1] < 10: |
| 195 | raise ValueError("invalid fill balls") |
| 196 | |
| 197 | return total_score |
| 198 | |
| 199 | # Handled Edge Cases: Game is already over, invalid pin count, too many pins in a frame, game not completed, incomplete frame data, not enough rolls for strike/spare calculation, invalid 10th frame, invalid fill balls |