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