| 1 | 1 | class BowlingGame: |
| 2 | 2 | def __init__(self): |
| 3 | | pass |
| 4 | | |
| 3 | """ |
| 4 | Initialize a 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 | :param pins: Number of pins knocked down in this roll. |
| 14 | :raises ValueError: If the number of pins is invalid. |
| 15 | """ |
| 16 | # Edge Case: Invalid number of pins (negative or more than 10) |
| 17 | if pins < 0 or pins > 10: |
| 18 | raise ValueError("Pins must be between 0 and 10") |
| 19 | |
| 20 | # Edge Case: Too many pins knocked down in a frame |
| 21 | if self.current_roll > 0 and self.rolls[-1] + pins > 10 and self.current_roll % 2 == 1 and self.current_roll < 18: |
| 22 | raise ValueError("Invalid roll: pins downed in a frame cannot exceed 10") |
| 23 | |
| 24 | # Edge Case: Game already has 10 complete frames |
| 25 | frame_index = self.current_roll // 2 |
| 26 | if frame_index >= 10: |
| 27 | # For the 10th frame, we need special handling |
| 28 | # Edge Case: Too many rolls in the 10th frame |
| 29 | if self.current_roll >= 21: |
| 30 | raise ValueError("Cannot roll after game is over") |
| 31 | |
| 32 | # Check if the current roll is valid in the 10th frame |
| 33 | if self.current_roll == 18: # First roll of 10th frame |
| 34 | self.rolls.append(pins) |
| 35 | self.current_roll += 1 |
| 36 | elif self.current_roll == 19: # Second roll of 10th frame |
| 37 | # If first roll was a strike, second roll can be anything up to 10 |
| 38 | if self.rolls[18] == 10: |
| 39 | self.rolls.append(pins) |
| 40 | self.current_roll += 1 |
| 41 | # Otherwise, sum of first two rolls cannot exceed 10 |
| 42 | elif self.rolls[18] + pins > 10: |
| 43 | raise ValueError("Invalid roll: pins downed in a frame cannot exceed 10") |
| 44 | else: |
| 45 | self.rolls.append(pins) |
| 46 | self.current_roll += 1 |
| 47 | elif self.current_roll == 20: # Third roll of 10th frame |
| 48 | # This is only allowed if previous rolls were a strike or spare |
| 49 | # Edge Case: Invalid third roll in 10th frame |
| 50 | if not ((self.rolls[18] == 10) or (self.rolls[18] + self.rolls[19] == 10)): |
| 51 | raise ValueError("Invalid roll: cannot have a third roll in 10th frame without strike or spare") |
| 52 | |
| 53 | # If second roll was a strike, third roll can be up to 10 |
| 54 | # Otherwise, if first+second was a spare, third roll can be up to 10 |
| 55 | # If first+second < 10, this shouldn't happen |
| 56 | self.rolls.append(pins) |
| 57 | self.current_roll += 1 |
| 58 | else: |
| 59 | self.rolls.append(pins) |
| 60 | self.current_roll += 1 |
| 61 | |
| 8 | 62 | def score(self): |
| 9 | | pass |
| 63 | """ |
| 64 | Calculate the total score for the game. |
| 65 | |
| 66 | :return: Total score of the game. |
| 67 | :raises ValueError: If the game is not complete. |
| 68 | """ |
| 69 | # Edge Case: Game not complete |
| 70 | if not self._is_game_complete(): |
| 71 | raise ValueError("Score cannot be taken until the end of the game") |
| 72 | |
| 73 | total_score = 0 |
| 74 | roll_index = 0 |
| 75 | |
| 76 | for frame in range(10): |
| 77 | # Edge Case: Strike in regular frames (1-9) |
| 78 | if frame < 9 and self._is_strike(roll_index): |
| 79 | total_score += 10 + self._strike_bonus(roll_index) |
| 80 | roll_index += 1 |
| 81 | # Edge Case: Spare in regular frames (1-9) |
| 82 | elif frame < 9 and self._is_spare(roll_index): |
| 83 | total_score += 10 + self._spare_bonus(roll_index) |
| 84 | roll_index += 2 |
| 85 | # Edge Case: Open frame in regular frames (1-9) |
| 86 | elif frame < 9: |
| 87 | total_score += self._sum_of_balls_in_frame(roll_index) |
| 88 | roll_index += 2 |
| 89 | # Edge Case: 10th frame scoring |
| 90 | else: # 10th frame |
| 91 | total_score += self._sum_of_balls_in_frame_10(roll_index) |
| 92 | # For 10th frame, we don't advance roll_index in the same way |
| 93 | # since it can have 2 or 3 rolls |
| 94 | break |
| 95 | |
| 96 | return total_score |
| 97 | |
| 98 | def _is_game_complete(self): |
| 99 | """ |
| 100 | Check if the game is complete. |
| 101 | |
| 102 | :return: True if the game is complete, False otherwise. |
| 103 | """ |
| 104 | # Game is complete when we have 10 frames |
| 105 | # For frames 1-9: 2 rolls each = 18 rolls |
| 106 | # For frame 10: 2 or 3 rolls depending on strike/spare |
| 107 | |
| 108 | # Edge Case: Not enough rolls |
| 109 | if self.current_roll < 18: |
| 110 | return False |
| 111 | |
| 112 | # Edge Case: Exactly 18 rolls (10 frames with no strike/spare in 10th frame) |
| 113 | if self.current_roll == 18: |
| 114 | return True |
| 115 | |
| 116 | # Edge Case: 19 rolls (first roll of 10th frame was a strike) |
| 117 | if self.current_roll == 19: |
| 118 | # Need one more roll |
| 119 | return False |
| 120 | |
| 121 | # Edge Case: 20 rolls (need to check if 10th frame was strike or spare) |
| 122 | if self.current_roll == 20: |
| 123 | # If 10th frame first roll was a strike or first two rolls were a spare |
| 124 | return (self.rolls[18] == 10) or (self.rolls[18] + self.rolls[19] == 10) |
| 125 | |
| 126 | # Edge Case: 21 rolls (complete 10th frame with strike/spare) |
| 127 | if self.current_roll == 21: |
| 128 | return True |
| 129 | |
| 130 | return False |
| 131 | |
| 132 | def _is_strike(self, roll_index): |
| 133 | """ |
| 134 | Check if the roll at roll_index is a strike. |
| 135 | |
| 136 | :param roll_index: Index of the roll to check. |
| 137 | :return: True if it's a strike, False otherwise. |
| 138 | """ |
| 139 | return self.rolls[roll_index] == 10 |
| 140 | |
| 141 | def _is_spare(self, roll_index): |
| 142 | """ |
| 143 | Check if the frame starting at roll_index is a spare. |
| 144 | |
| 145 | :param roll_index: Index of the first roll of the frame. |
| 146 | :return: True if it's a spare, False otherwise. |
| 147 | """ |
| 148 | return self.rolls[roll_index] + self.rolls[roll_index + 1] == 10 |
| 149 | |
| 150 | def _strike_bonus(self, roll_index): |
| 151 | """ |
| 152 | Calculate the bonus for a strike. |
| 153 | |
| 154 | :param roll_index: Index of the strike roll. |
| 155 | :return: Bonus points for the strike. |
| 156 | """ |
| 157 | return self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 158 | |
| 159 | def _spare_bonus(self, roll_index): |
| 160 | """ |
| 161 | Calculate the bonus for a spare. |
| 162 | |
| 163 | :param roll_index: Index of the first roll of the spare frame. |
| 164 | :return: Bonus points for the spare. |
| 165 | """ |
| 166 | return self.rolls[roll_index + 2] |
| 167 | |
| 168 | def _sum_of_balls_in_frame(self, roll_index): |
| 169 | """ |
| 170 | Calculate the sum of pins knocked down in a frame (for frames 1-9). |
| 171 | |
| 172 | :param roll_index: Index of the first roll of the frame. |
| 173 | :return: Sum of pins in the frame. |
| 174 | """ |
| 175 | return self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 176 | |
| 177 | def _sum_of_balls_in_frame_10(self, roll_index): |
| 178 | """ |
| 179 | Calculate the sum of pins knocked down in the 10th frame. |
| 180 | |
| 181 | :param roll_index: Index of the first roll of the 10th frame (should be 18). |
| 182 | :return: Sum of pins in the 10th frame. |
| 183 | """ |
| 184 | # For the 10th frame, we sum all rolls in the frame |
| 185 | # This could be 2 or 3 rolls depending on strike/spare |
| 186 | if self.current_roll >= 21: # Three rolls in 10th frame |
| 187 | return self.rolls[18] + self.rolls[19] + self.rolls[20] |
| 188 | elif self.current_roll >= 20: # Two rolls in 10th frame |
| 189 | return self.rolls[18] + self.rolls[19] |
| 190 | else: # Should not happen if game is complete |
| 191 | return self.rolls[18] + self.rolls[19] |
| 192 | |
| 193 | # Handled Edge Cases: Invalid number of pins, too many pins in a frame, game already complete, too many rolls in 10th frame, invalid third roll in 10th frame, game not complete when scoring, strike in regular frames, spare in regular frames, open frame in regular frames, 10th frame scoring, not enough rolls, exactly 18 rolls, 19 rolls, 20 rolls, 21 rolls |