| 1 | 1 | class BowlingGame: |
| 2 | 2 | def __init__(self): |
| 3 | | pass |
| 3 | self.rolls = [] |
| 4 | 4 | |
| 5 | 5 | def roll(self, pins): |
| 6 | | pass |
| 6 | # Edge Case: Invalid pin count (negative or over 10) |
| 7 | if pins < 0 or pins > 10: |
| 8 | raise ValueError("pins must be between 0 and 10") |
| 9 | |
| 10 | # Edge Case: Game already completed |
| 11 | if self.is_game_complete(): |
| 12 | raise ValueError("cannot roll after game is over") |
| 13 | |
| 14 | # Edge Case: Two rolls in a frame cannot exceed 10 points |
| 15 | if not self.is_valid_roll(pins): |
| 16 | raise ValueError("invalid roll") |
| 17 | |
| 18 | self.rolls.append(pins) |
| 7 | 19 | |
| 8 | 20 | def score(self): |
| 9 | | pass |
| 21 | # Edge Case: Game not yet completed |
| 22 | if not self.is_game_complete(): |
| 23 | raise ValueError("game not yet complete") |
| 24 | |
| 25 | score = 0 |
| 26 | roll_index = 0 |
| 27 | |
| 28 | for frame in range(10): |
| 29 | if frame < 9: # Frames 1-9 |
| 30 | if self.is_strike(roll_index): |
| 31 | score += 10 + self.strike_bonus(roll_index) |
| 32 | roll_index += 1 |
| 33 | elif self.is_spare(roll_index): |
| 34 | score += 10 + self.spare_bonus(roll_index) |
| 35 | roll_index += 2 |
| 36 | else: |
| 37 | score += self.sum_of_balls_in_frame(roll_index) |
| 38 | roll_index += 2 |
| 39 | else: # Frame 10 (special case) |
| 40 | frame_10_rolls = self.get_frame_10_rolls() |
| 41 | score += sum(frame_10_rolls) |
| 42 | |
| 43 | return score |
| 44 | |
| 45 | def is_valid_roll(self, pins): |
| 46 | """Check if the current roll is valid based on frame rules""" |
| 47 | if len(self.rolls) == 0: |
| 48 | return True |
| 49 | |
| 50 | # Find current frame |
| 51 | frame_index, roll_in_frame = self.get_current_frame_and_roll() |
| 52 | |
| 53 | # 10th frame has special rules |
| 54 | if frame_index == 9: |
| 55 | return self.is_valid_10th_frame_roll(pins, roll_in_frame) |
| 56 | |
| 57 | # Frames 1-9 |
| 58 | if roll_in_frame == 0: |
| 59 | return True # First roll of frame can be 0-10 |
| 60 | else: |
| 61 | # Second roll - can't exceed 10 total in frame |
| 62 | frame_start = len(self.rolls) - 1 |
| 63 | return self.rolls[frame_start] + pins <= 10 |
| 64 | |
| 65 | def get_current_frame_and_roll(self): |
| 66 | """Get current frame index (0-9) and roll in frame (0 or 1)""" |
| 67 | frame_index = 0 |
| 68 | roll_index = 0 |
| 69 | |
| 70 | while frame_index < 9 and roll_index < len(self.rolls): |
| 71 | if self.rolls[roll_index] == 10: # Strike |
| 72 | frame_index += 1 |
| 73 | roll_index += 1 |
| 74 | else: |
| 75 | if roll_index + 1 < len(self.rolls): |
| 76 | frame_index += 1 |
| 77 | roll_index += 2 |
| 78 | else: |
| 79 | # We're in the middle of a frame |
| 80 | return frame_index, 1 |
| 81 | |
| 82 | if frame_index >= 9: |
| 83 | # We're in 10th frame |
| 84 | rolls_in_10th = len(self.rolls) - roll_index |
| 85 | return 9, rolls_in_10th |
| 86 | else: |
| 87 | return frame_index, 0 |
| 88 | |
| 89 | def is_valid_10th_frame_roll(self, pins, roll_in_frame): |
| 90 | """Validate rolls in the 10th frame""" |
| 91 | frame_10_rolls = self.get_frame_10_rolls() |
| 92 | |
| 93 | if roll_in_frame == 0: |
| 94 | return True # First roll can be 0-10 |
| 95 | elif roll_in_frame == 1: |
| 96 | if frame_10_rolls[0] == 10: # First roll was strike |
| 97 | return True # Second roll after strike can be 0-10 |
| 98 | else: |
| 99 | return frame_10_rolls[0] + pins <= 10 # Can't exceed 10 |
| 100 | elif roll_in_frame == 2: |
| 101 | # Third roll only if first was strike or first two were spare |
| 102 | if frame_10_rolls[0] == 10: # First was strike |
| 103 | if frame_10_rolls[1] == 10: # Second was also strike |
| 104 | return True # Third can be 0-10 |
| 105 | else: |
| 106 | return frame_10_rolls[1] + pins <= 10 # Can't exceed 10 |
| 107 | else: # First two were spare |
| 108 | return True # Third roll after spare can be 0-10 |
| 109 | else: |
| 110 | return False # Shouldn't get here |
| 111 | |
| 112 | def is_game_complete(self): |
| 113 | """Check if the game is complete""" |
| 114 | if len(self.rolls) < 12: # Minimum rolls (all strikes) |
| 115 | return False |
| 116 | |
| 117 | frame_index, roll_index = 0, 0 |
| 118 | |
| 119 | # Count frames |
| 120 | while frame_index < 10 and roll_index < len(self.rolls): |
| 121 | if frame_index < 9: # Frames 1-9 |
| 122 | if self.rolls[roll_index] == 10: # Strike |
| 123 | frame_index += 1 |
| 124 | roll_index += 1 |
| 125 | else: |
| 126 | if roll_index + 1 < len(self.rolls): |
| 127 | frame_index += 1 |
| 128 | roll_index += 2 |
| 129 | else: |
| 130 | return False # Incomplete frame |
| 131 | else: # Frame 10 |
| 132 | frame_10_rolls = self.get_frame_10_rolls() |
| 133 | if len(frame_10_rolls) == 0: |
| 134 | return False |
| 135 | elif len(frame_10_rolls) == 1: |
| 136 | return False |
| 137 | elif len(frame_10_rolls) == 2: |
| 138 | # Complete if no strike/spare, or if strike/spare but no more rolls needed |
| 139 | first = frame_10_rolls[0] |
| 140 | second = frame_10_rolls[1] |
| 141 | if first == 10 or first + second == 10: |
| 142 | return False # Need third roll |
| 143 | else: |
| 144 | return True # Open frame, complete |
| 145 | else: # 3 rolls |
| 146 | return True |
| 147 | |
| 148 | # If we processed all 10 frames, game is complete |
| 149 | # But only if we actually have 10 frames worth of rolls |
| 150 | if frame_index >= 10: |
| 151 | return True |
| 152 | else: |
| 153 | return False |
| 154 | |
| 155 | def get_frame_10_rolls(self): |
| 156 | # Get the rolls that belong to the 10th frame |
| 157 | frame_10_rolls = [] |
| 158 | roll_index = 0 |
| 159 | |
| 160 | for frame in range(9): # First 9 frames |
| 161 | if roll_index >= len(self.rolls): |
| 162 | break |
| 163 | if self.is_strike(roll_index): |
| 164 | roll_index += 1 |
| 165 | else: |
| 166 | roll_index += 2 |
| 167 | |
| 168 | # Now roll_index points to the start of 10th frame |
| 169 | # Collect 10th frame rolls |
| 170 | while roll_index < len(self.rolls): |
| 171 | frame_10_rolls.append(self.rolls[roll_index]) |
| 172 | roll_index += 1 |
| 173 | |
| 174 | return frame_10_rolls |
| 175 | |
| 176 | def is_strike(self, roll_index): |
| 177 | return self.rolls[roll_index] == 10 |
| 178 | |
| 179 | def is_spare(self, roll_index): |
| 180 | return self.rolls[roll_index] + self.rolls[roll_index + 1] == 10 |
| 181 | |
| 182 | def strike_bonus(self, roll_index): |
| 183 | return self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 184 | |
| 185 | def spare_bonus(self, roll_index): |
| 186 | return self.rolls[roll_index + 2] |
| 187 | |
| 188 | def sum_of_balls_in_frame(self, roll_index): |
| 189 | return self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 190 | |
| 191 | # Handled Edge Cases: invalid pin counts (negative or over 10), game already completed, invalid roll after strike, game not yet completed when scoring, incomplete game when scoring, not enough rolls for strike bonus calculation, not enough rolls for spare bonus calculation |