| 1 | 1 | class BowlingGame: |
| 2 | 2 | def __init__(self): |
| 3 | | pass |
| 4 | | |
| 3 | """ |
| 4 | Initialize a new bowling game. |
| 5 | |
| 6 | Sets up the game state including rolls tracking, current frame, and game completion status. |
| 7 | """ |
| 8 | self.rolls = [] |
| 9 | self.current_frame = 0 |
| 10 | self.is_finished = False |
| 11 | self.frame_rolls = [] # Track rolls in the current frame |
| 12 | |
| 5 | 13 | def roll(self, pins): |
| 6 | | pass |
| 7 | | |
| 14 | """ |
| 15 | Record a roll in the bowling game. |
| 16 | |
| 17 | Args: |
| 18 | pins (int): Number of pins knocked down in this roll |
| 19 | |
| 20 | Raises: |
| 21 | ValueError: If pins is negative, greater than 10, or if game is already finished |
| 22 | IndexError: If attempting invalid rolls in the 10th frame |
| 23 | """ |
| 24 | # Edge Case: Game already finished |
| 25 | if self.is_finished: |
| 26 | raise IndexError("Game has already ended") |
| 27 | |
| 28 | # Edge Case: Invalid pin count (negative) |
| 29 | if pins < 0: |
| 30 | raise ValueError("Pins must be between 0 and 10") |
| 31 | |
| 32 | # Edge Case: Invalid pin count (more than 10) |
| 33 | if pins > 10: |
| 34 | raise ValueError("Pins must be between 0 and 10") |
| 35 | |
| 36 | # Handle 10th frame special cases |
| 37 | if self.current_frame == 9: # 10th frame (0-indexed) |
| 38 | return self._handle_tenth_frame(pins) |
| 39 | |
| 40 | # Regular frames (1-9) |
| 41 | self.frame_rolls.append(pins) |
| 42 | self.rolls.append(pins) |
| 43 | |
| 44 | # Edge Case: First roll is a strike |
| 45 | if len(self.frame_rolls) == 1 and pins == 10: |
| 46 | self.current_frame += 1 |
| 47 | self.frame_rolls = [] |
| 48 | # Edge Case: Second roll completes a frame |
| 49 | elif len(self.frame_rolls) == 2: |
| 50 | # Edge Case: Frame total exceeds 10 pins (except in 10th frame) |
| 51 | if sum(self.frame_rolls) > 10: |
| 52 | raise ValueError("Cannot knock down more than 10 pins in a frame") |
| 53 | self.current_frame += 1 |
| 54 | self.frame_rolls = [] |
| 55 | |
| 56 | # Edge Case: Game completed after 10 frames |
| 57 | if self.current_frame == 10: |
| 58 | self.is_finished = True |
| 59 | |
| 60 | def _handle_tenth_frame(self, pins): |
| 61 | """ |
| 62 | Handle special rules for the 10th frame. |
| 63 | |
| 64 | Args: |
| 65 | pins (int): Number of pins knocked down in this roll |
| 66 | """ |
| 67 | self.frame_rolls.append(pins) |
| 68 | self.rolls.append(pins) |
| 69 | |
| 70 | # Edge Case: Too many rolls in 10th frame |
| 71 | if len(self.frame_rolls) > 3: |
| 72 | raise IndexError("cannot throw bonus with an open tenth frame") |
| 73 | |
| 74 | # If we have 2 rolls |
| 75 | if len(self.frame_rolls) == 2: |
| 76 | # Edge Case: Open frame in 10th frame with invalid third roll |
| 77 | frame_sum = sum(self.frame_rolls) |
| 78 | if frame_sum < 10: |
| 79 | self.is_finished = True |
| 80 | return |
| 81 | |
| 82 | # If we have 3 rolls |
| 83 | if len(self.frame_rolls) == 3: |
| 84 | # Edge Case: Invalid fill balls |
| 85 | # Check if the scoring is valid |
| 86 | first_roll = self.frame_rolls[0] |
| 87 | second_roll = self.frame_rolls[1] |
| 88 | |
| 89 | # If first roll was a strike |
| 90 | if first_roll == 10: |
| 91 | # Second roll can be anything, third roll depends on second |
| 92 | if second_roll == 10 or (second_roll + self.frame_rolls[2]) <= 10: |
| 93 | self.is_finished = True |
| 94 | return |
| 95 | else: |
| 96 | raise ValueError("Pins must be between 0 and 10") |
| 97 | # If first roll wasn't a strike but second completed a spare |
| 98 | elif first_roll + second_roll == 10: |
| 99 | # Third roll must be valid |
| 100 | if self.frame_rolls[2] <= 10: |
| 101 | self.is_finished = True |
| 102 | return |
| 103 | else: |
| 104 | raise ValueError("Pins must be between 0 and 10") |
| 105 | else: |
| 106 | # This shouldn't happen as we should have finished after 2 rolls for open frame |
| 107 | raise ValueError("Pins must be between 0 and 10") |
| 108 | |
| 8 | 109 | def score(self): |
| 9 | | pass |
| 110 | """ |
| 111 | Calculate the total score for the bowling game. |
| 112 | |
| 113 | Returns: |
| 114 | int: Total score for the game |
| 115 | |
| 116 | Raises: |
| 117 | IndexError: If trying to score a game that isn't finished |
| 118 | """ |
| 119 | # Edge Case: Attempting to score unfinished game |
| 120 | if not self.is_finished: |
| 121 | raise IndexError("Game is not yet complete") |
| 122 | |
| 123 | total_score = 0 |
| 124 | roll_index = 0 |
| 125 | |
| 126 | # Score frames 1-9 |
| 127 | for frame in range(9): # First 9 frames |
| 128 | # Edge Case: Strike |
| 129 | if self.rolls[roll_index] == 10: # Strike |
| 130 | total_score += 10 + self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 131 | roll_index += 1 |
| 132 | else: |
| 133 | frame_score = self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 134 | # Edge Case: Spare |
| 135 | if frame_score == 10: # Spare |
| 136 | total_score += 10 + self.rolls[roll_index + 2] |
| 137 | else: # Open frame |
| 138 | total_score += frame_score |
| 139 | roll_index += 2 |
| 140 | |
| 141 | # Score 10th frame |
| 142 | # Edge Case: Handle 10th frame scoring |
| 143 | # Add the remaining rolls (10th frame rolls) to the total score |
| 144 | while roll_index < len(self.rolls): |
| 145 | total_score += self.rolls[roll_index] |
| 146 | roll_index += 1 |
| 147 | |
| 148 | return total_score |
| 149 | |
| 150 | # Handled Edge Cases: Game already finished, invalid pin count (negative), invalid pin count (more than 10), first roll is a strike, second roll completes a frame, frame total exceeds 10 pins, game completed after 10 frames, too many rolls in 10th frame, open frame in 10th frame with invalid third roll, invalid fill balls, attempting to score unfinished game, strike, spare |