| 1 | 1 | class BowlingGame: |
| 2 | 2 | def __init__(self): |
| 3 | | pass |
| 4 | | |
| 3 | """ |
| 4 | Initialize a new bowling game. |
| 5 | """ |
| 6 | self.rolls = [] |
| 7 | |
| 5 | 8 | def roll(self, pins): |
| 6 | | pass |
| 9 | """ |
| 10 | Record a roll in the game. |
| 11 | |
| 12 | Args: |
| 13 | pins (int): Number of pins knocked down in this roll |
| 14 | |
| 15 | Raises: |
| 16 | ValueError: If the number of pins is invalid or game is already finished |
| 17 | """ |
| 18 | # Check if game is already finished |
| 19 | if self._is_game_finished(): |
| 20 | raise ValueError("Cannot roll after game is over") |
| 21 | |
| 22 | # Validate pins |
| 23 | if pins < 0 or pins > 10: |
| 24 | raise ValueError("Pins must be between 0 and 10") |
| 25 | |
| 26 | # Validate frame rules for frames 1-9 |
| 27 | if len(self.rolls) < 18: # Frames 1-9 (max 18 rolls) |
| 28 | # For frames 1-9, check if this roll exceeds 10 pins in the frame |
| 29 | if len(self.rolls) % 2 == 1: # Second roll of a frame (frames 1-9) |
| 30 | if self.rolls[-1] + pins > 10: |
| 31 | raise ValueError("Invalid roll: pins downed in a frame cannot exceed 10") |
| 32 | |
| 33 | # Validate frame rules for frame 10 |
| 34 | elif len(self.rolls) >= 18: # Frame 10 rolls |
| 35 | if len(self.rolls) == 18: # First roll of 10th frame |
| 36 | # No validation needed |
| 37 | pass |
| 38 | elif len(self.rolls) == 19: # Second roll of 10th frame |
| 39 | first_roll = self.rolls[18] |
| 40 | if first_roll != 10 and first_roll + pins > 10: |
| 41 | raise ValueError("Invalid roll: pins downed in a frame cannot exceed 10") |
| 42 | elif len(self.rolls) == 20: # Third roll of 10th frame (fill ball) |
| 43 | # Only allowed if first was strike or first two were spare |
| 44 | first_roll = self.rolls[18] |
| 45 | second_roll = self.rolls[19] |
| 46 | # Check if we actually should have a third roll |
| 47 | if first_roll != 10 and first_roll + second_roll != 10: |
| 48 | raise ValueError("Invalid fill balls") |
| 49 | |
| 50 | self.rolls.append(pins) |
| 51 | |
| 7 | 52 | |
| 53 | |
| 54 | def _is_game_finished(self): |
| 55 | """ |
| 56 | Check if the game is finished. |
| 57 | |
| 58 | Returns: |
| 59 | bool: True if the game is finished, False otherwise |
| 60 | """ |
| 61 | # Game can't be finished with fewer than 18 rolls (9 frames * 2 rolls) |
| 62 | if len(self.rolls) < 18: |
| 63 | return False |
| 64 | |
| 65 | # Check 10th frame status |
| 66 | if len(self.rolls) == 18: # Only first roll of 10th frame |
| 67 | # Game is not finished with only one roll in 10th frame |
| 68 | return False |
| 69 | elif len(self.rolls) == 19: # Only first two rolls of 10th frame exist |
| 70 | # This case should not be reached because we check len(self.rolls) == 19 |
| 71 | # But if it is, we have 19 rolls (indices 0-18) |
| 72 | # Roll 18 is the first roll of frame 10 |
| 73 | # There is no roll 19 because we only have 19 rolls total |
| 74 | # This means we only have one roll in frame 10, so game is not finished |
| 75 | return False |
| 76 | elif len(self.rolls) == 20: # Two rolls in 10th frame |
| 77 | # Game is finished only if it's an open frame (not a spare or strike) |
| 78 | first_roll = self.rolls[18] |
| 79 | second_roll = self.rolls[19] |
| 80 | return first_roll + second_roll < 10 |
| 81 | elif len(self.rolls) == 21: # Three rolls in 10th frame |
| 82 | # Game is always finished with three rolls (strike or spare with fill balls) |
| 83 | return True |
| 84 | elif len(self.rolls) > 20: # More than 20 rolls (shouldn't happen in valid games) |
| 85 | return True |
| 86 | |
| 87 | return False |
| 88 | |
| 8 | 89 | def score(self): |
| 9 | | pass |
| 90 | """ |
| 91 | Calculate the total score for the game. |
| 92 | |
| 93 | Returns: |
| 94 | int: The total score of the game |
| 95 | |
| 96 | Raises: |
| 97 | ValueError: If the game is not yet complete |
| 98 | """ |
| 99 | # Check if game is finished |
| 100 | if not self._is_game_finished(): |
| 101 | raise ValueError("Score cannot be taken until the end of the game") |
| 102 | |
| 103 | total_score = 0 |
| 104 | roll_index = 0 |
| 105 | |
| 106 | # Score first 9 frames |
| 107 | for frame in range(9): |
| 108 | if roll_index >= len(self.rolls): |
| 109 | break |
| 110 | |
| 111 | if self.rolls[roll_index] == 10: # Strike |
| 112 | # Add bonus for next two rolls |
| 113 | if roll_index + 2 < len(self.rolls): |
| 114 | total_score += 10 + self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 115 | else: |
| 116 | # This shouldn't happen if game is finished |
| 117 | raise ValueError("Invalid game state: not enough rolls for strike bonus") |
| 118 | roll_index += 1 |
| 119 | else: |
| 120 | frame_score = self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 121 | if frame_score == 10: # Spare |
| 122 | # Add bonus for next roll |
| 123 | if roll_index + 2 < len(self.rolls): |
| 124 | total_score += 10 + self.rolls[roll_index + 2] |
| 125 | else: |
| 126 | # This shouldn't happen if game is finished |
| 127 | raise ValueError("Invalid game state: not enough rolls for spare bonus") |
| 128 | else: # Open frame |
| 129 | total_score += frame_score |
| 130 | roll_index += 2 |
| 131 | |
| 132 | # Score tenth frame - just sum all remaining rolls (rolls 18, 19, and possibly 20) |
| 133 | while roll_index < len(self.rolls): |
| 134 | total_score += self.rolls[roll_index] |
| 135 | roll_index += 1 |
| 136 | |
| 137 | return total_score |