| 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) |
| 20 | if pins < 0: |
| 21 | raise ValueError("Pins must be between 0 and 10") |
| 22 | |
| 23 | # Edge Case: Invalid pin count (more than 10) |
| 24 | if pins > 10: |
| 25 | raise ValueError("Pins must be between 0 and 10") |
| 26 | |
| 27 | # Edge Case: Check if game is already complete |
| 28 | if self._is_game_complete(): |
| 29 | raise IndexError("Game is already complete") |
| 30 | |
| 31 | # Edge Case: In a frame, the sum of pins from two rolls exceeds 10 |
| 32 | # This only applies to non-strike frames |
| 33 | if self.current_roll % 2 == 1: # Second roll of a frame |
| 34 | # But not in the 10th frame, where special rules apply |
| 35 | frame_index = self.current_roll // 2 |
| 36 | if frame_index < 9: # Not the 10th frame |
| 37 | # Only check if the first roll wasn't a strike |
| 38 | if self.rolls[-1] != 10 and self.rolls[-1] + pins > 10: |
| 39 | raise ValueError("Cannot knock down more pins than remaining") |
| 40 | |
| 41 | # Edge Case: Special validation for 10th frame fill balls |
| 42 | if len(self.rolls) >= 18: # We're in the 10th frame |
| 43 | tenth_frame_rolls = self.rolls[18:] |
| 44 | |
| 45 | # If first roll was a strike and we have a second roll |
| 46 | if len(tenth_frame_rolls) == 1 and tenth_frame_rolls[0] == 10: |
| 47 | # Second roll after strike can be 0-10 |
| 48 | pass # No validation needed here |
| 49 | |
| 50 | # If first roll was a strike and we have a second roll, now adding third |
| 51 | elif len(tenth_frame_rolls) == 2 and tenth_frame_rolls[0] == 10: |
| 52 | # If second roll was also a strike, third roll can be 0-10 |
| 53 | if tenth_frame_rolls[1] == 10: |
| 54 | pass # No validation needed |
| 55 | # If second roll wasn't a strike, third roll can't exceed remaining pins |
| 56 | else: |
| 57 | if tenth_frame_rolls[1] + pins > 10: |
| 58 | raise ValueError("Cannot knock down more pins than remaining") |
| 59 | |
| 60 | # If we have two rolls that make a spare, adding third roll |
| 61 | elif len(tenth_frame_rolls) == 2 and tenth_frame_rolls[0] + tenth_frame_rolls[1] == 10: |
| 62 | # Third roll after spare can be 0-10 |
| 63 | pass # No validation needed |
| 64 | |
| 65 | self.rolls.append(pins) |
| 66 | self.current_roll += 1 |
| 67 | |
| 8 | 68 | def score(self): |
| 9 | | pass |
| 69 | """ |
| 70 | Calculate the total score for the game. |
| 71 | |
| 72 | Returns: |
| 73 | int: The total score of the game. |
| 74 | |
| 75 | Raises: |
| 76 | IndexError: If the game is not yet complete. |
| 77 | """ |
| 78 | # Edge Case: Game is not complete |
| 79 | if not self._is_game_complete(): |
| 80 | raise IndexError("Game is not yet complete") |
| 81 | |
| 82 | total_score = 0 |
| 83 | roll_index = 0 |
| 84 | |
| 85 | for frame in range(10): |
| 86 | # Edge Case: Strike in frames 1-9 |
| 87 | if frame < 9 and roll_index < len(self.rolls) and self.rolls[roll_index] == 10: # Strike |
| 88 | total_score += 10 + self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 89 | roll_index += 1 |
| 90 | # Edge Case: Spare in frames 1-9 |
| 91 | elif frame < 9 and roll_index + 1 < len(self.rolls) and self.rolls[roll_index] + self.rolls[roll_index + 1] == 10: # Spare |
| 92 | total_score += 10 + self.rolls[roll_index + 2] |
| 93 | roll_index += 2 |
| 94 | # Edge Case: Open frame in frames 1-9 |
| 95 | elif frame < 9: # Open frame |
| 96 | total_score += self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 97 | roll_index += 2 |
| 98 | # Edge Case: 10th frame - special handling |
| 99 | else: |
| 100 | # Handle the 10th frame differently |
| 101 | if roll_index < len(self.rolls): |
| 102 | # If we have enough rolls for the 10th frame |
| 103 | if frame == 9: |
| 104 | # 10th frame scoring - just sum all remaining rolls |
| 105 | while roll_index < len(self.rolls): |
| 106 | total_score += self.rolls[roll_index] |
| 107 | roll_index += 1 |
| 108 | else: |
| 109 | # Regular frame |
| 110 | total_score += self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 111 | roll_index += 2 |
| 112 | |
| 113 | return total_score |
| 114 | |
| 115 | def _is_game_complete(self): |
| 116 | """ |
| 117 | Check if the game is complete. |
| 118 | |
| 119 | Returns: |
| 120 | bool: True if the game is complete, False otherwise. |
| 121 | """ |
| 122 | # Need at least 10 frames |
| 123 | if len(self.rolls) < 10: |
| 124 | return False |
| 125 | |
| 126 | # Count frames properly |
| 127 | frame_count = 0 |
| 128 | roll_index = 0 |
| 129 | |
| 130 | # Count the first 9 frames |
| 131 | while frame_count < 9 and roll_index < len(self.rolls): |
| 132 | if self.rolls[roll_index] == 10: # Strike |
| 133 | frame_count += 1 |
| 134 | roll_index += 1 |
| 135 | elif roll_index + 1 < len(self.rolls): # Regular frame |
| 136 | frame_count += 1 |
| 137 | roll_index += 2 |
| 138 | else: # Incomplete frame |
| 139 | return False |
| 140 | |
| 141 | # If we haven't completed 9 frames, game is not complete |
| 142 | if frame_count < 9: |
| 143 | return False |
| 144 | |
| 145 | # Now handle the 10th frame |
| 146 | if roll_index >= len(self.rolls): |
| 147 | return False # No rolls for 10th frame yet |
| 148 | |
| 149 | tenth_frame_first = self.rolls[roll_index] |
| 150 | |
| 151 | # Strike in 10th frame - need 2 fill balls |
| 152 | if tenth_frame_first == 10: |
| 153 | if roll_index + 2 >= len(self.rolls): |
| 154 | return False # Need 2 more rolls |
| 155 | elif roll_index + 2 == len(self.rolls) - 1: |
| 156 | # Have first roll and one fill ball |
| 157 | return False |
| 158 | else: |
| 159 | # Have first roll and 2 fill balls |
| 160 | return roll_index + 3 == len(self.rolls) |
| 161 | |
| 162 | # Need second roll for 10th frame |
| 163 | if roll_index + 1 >= len(self.rolls): |
| 164 | return False |
| 165 | |
| 166 | tenth_frame_second = self.rolls[roll_index + 1] |
| 167 | |
| 168 | # Spare in 10th frame - need 1 fill ball |
| 169 | if tenth_frame_first + tenth_frame_second == 10: |
| 170 | return roll_index + 3 == len(self.rolls) |
| 171 | |
| 172 | # Open frame in 10th frame - no fill balls needed |
| 173 | else: |
| 174 | return roll_index + 2 == len(self.rolls) |
| 175 | |
| 176 | def _is_valid_fill_ball(self): |
| 177 | """ |
| 178 | Check if a roll is a valid fill ball. |
| 179 | |
| 180 | Returns: |
| 181 | bool: True if the roll is a valid fill ball, False otherwise. |
| 182 | """ |
| 183 | # This method is no longer needed since we check game completion first |
| 184 | return False |
| 185 | |
| 186 | # Handled Edge Cases: Invalid pin count (negative or more than 10), Game already has 20 rolls (10 frames completed), In a frame, the sum of pins from two rolls exceeds 10, Game is not complete, Less than 10 frames played, Strike in frames 1-9, Spare in frames 1-9, Open frame in frames 1-9, Trying to roll after game is complete but it's a valid fill ball, Must have at least 18 rolls to be in the 10th frame, If we have exactly 18 rolls we're starting the 10th frame, If we have 19 rolls check if we need a fill ball, If we have 20 rolls check if we need another fill ball, If we have 21 rolls we're done |