| 1 | 1 | class BowlingGame: |
| 2 | 2 | def __init__(self): |
| 3 | | pass |
| 3 | self.rolls = [] |
| 4 | 4 | |
| 5 | 5 | def roll(self, pins): |
| 6 | | pass |
| 6 | # Edge Case: Game is already over |
| 7 | if self.is_game_over(): |
| 8 | raise ValueError("Cannot roll after game is over") |
| 9 | |
| 10 | # Edge Case: Invalid pin count (negative or more than 10) |
| 11 | if pins < 0 or pins > 10: |
| 12 | raise ValueError("Pins must be between 0 and 10") |
| 13 | |
| 14 | # Edge Case: Too many pins in a single frame (except in special tenth frame cases) |
| 15 | # Simple validation: if the last roll wasn't a strike and we haven't completed 9 frames yet |
| 16 | if len(self.rolls) > 0 and self.rolls[-1] != 10: |
| 17 | # Count how many frames we've completed so far |
| 18 | frames_completed = 0 |
| 19 | roll_index = 0 |
| 20 | while roll_index < len(self.rolls) and frames_completed < 9: |
| 21 | if self.rolls[roll_index] == 10: # Strike |
| 22 | roll_index += 1 |
| 23 | frames_completed += 1 |
| 24 | elif roll_index + 1 < len(self.rolls): # Complete frame with 2 rolls |
| 25 | roll_index += 2 |
| 26 | frames_completed += 1 |
| 27 | else: # Incomplete frame |
| 28 | break |
| 29 | |
| 30 | # If we're still in frames 1-9 and the last roll was the first of a frame |
| 31 | if frames_completed < 9 and roll_index == len(self.rolls) - 1: |
| 32 | if self.rolls[-1] + pins > 10: |
| 33 | raise ValueError("Invalid roll: pins downed in a frame cannot exceed 10") |
| 34 | |
| 35 | self.rolls.append(pins) |
| 36 | |
| 37 | def get_frame_start_index(self, frame_num): |
| 38 | """Get the starting roll index for a given frame number (0-indexed)""" |
| 39 | if frame_num == 0: |
| 40 | return 0 |
| 41 | |
| 42 | roll_index = 0 |
| 43 | current_frame = 0 |
| 44 | |
| 45 | # Count frames until we reach the target frame |
| 46 | while current_frame < frame_num and roll_index < len(self.rolls): |
| 47 | if self.rolls[roll_index] == 10: # Strike |
| 48 | roll_index += 1 |
| 49 | else: # Open frame or spare |
| 50 | roll_index += 2 |
| 51 | current_frame += 1 |
| 52 | |
| 53 | return roll_index |
| 54 | |
| 55 | def get_current_frame(self): |
| 56 | """Calculate the current frame based on number of rolls""" |
| 57 | # For frames 1-9, each frame has 1 or 2 rolls |
| 58 | # For frame 10, it can have 2 or 3 rolls |
| 59 | frame = 0 |
| 60 | roll_index = 0 |
| 61 | |
| 62 | # Count complete frames 1-9 |
| 63 | while frame < 9 and roll_index < len(self.rolls): |
| 64 | if self.rolls[roll_index] == 10: # Strike |
| 65 | roll_index += 1 |
| 66 | else: # Open frame or spare |
| 67 | roll_index += 2 |
| 68 | frame += 1 |
| 69 | |
| 70 | return frame |
| 71 | |
| 72 | def is_game_over(self): |
| 73 | """Check if the game is over""" |
| 74 | # Count frames to see if we've completed 10 frames |
| 75 | frame = 0 |
| 76 | roll_index = 0 |
| 77 | |
| 78 | # Process frames 1-9 |
| 79 | while frame < 9 and roll_index < len(self.rolls): |
| 80 | if self.rolls[roll_index] == 10: # Strike |
| 81 | roll_index += 1 |
| 82 | else: # Open frame or spare |
| 83 | roll_index += 2 |
| 84 | frame += 1 |
| 85 | |
| 86 | # If we haven't processed 9 complete frames, game is not over |
| 87 | if frame < 9: |
| 88 | return False |
| 89 | |
| 90 | # Now check the 10th frame |
| 91 | if roll_index >= len(self.rolls): |
| 92 | return False # No rolls in 10th frame yet |
| 93 | |
| 94 | # 10th frame logic |
| 95 | tenth_frame_rolls = [] |
| 96 | temp_index = roll_index |
| 97 | while temp_index < len(self.rolls): |
| 98 | tenth_frame_rolls.append(self.rolls[temp_index]) |
| 99 | temp_index += 1 |
| 100 | |
| 101 | # If 10th frame has less than 2 rolls, not over |
| 102 | if len(tenth_frame_rolls) < 2: |
| 103 | return False |
| 104 | |
| 105 | # If 10th frame is a strike or spare, need 3 rolls |
| 106 | if tenth_frame_rolls[0] == 10 or sum(tenth_frame_rolls[:2]) == 10: |
| 107 | return len(tenth_frame_rolls) == 3 |
| 108 | else: |
| 109 | # Regular open frame, 2 rolls is enough |
| 110 | return len(tenth_frame_rolls) == 2 |
| 7 | 111 | |
| 8 | 112 | def score(self): |
| 9 | | pass |
| 113 | # Edge Case: Game is not yet complete |
| 114 | if len(self.rolls) > 0 and not self.is_game_over(): |
| 115 | raise ValueError("Score cannot be taken until the end of the game") |
| 116 | |
| 117 | # If no rolls have been made, score is 0 |
| 118 | if len(self.rolls) == 0: |
| 119 | return 0 |
| 120 | |
| 121 | total_score = 0 |
| 122 | roll_index = 0 |
| 123 | |
| 124 | # Score first 9 frames |
| 125 | for frame in range(9): |
| 126 | # Check if we have enough rolls left |
| 127 | if roll_index >= len(self.rolls): |
| 128 | break |
| 129 | |
| 130 | # Strike |
| 131 | if self.rolls[roll_index] == 10: |
| 132 | # Need at least 2 more rolls for bonus |
| 133 | if roll_index + 2 < len(self.rolls): |
| 134 | total_score += 10 + self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 135 | roll_index += 1 |
| 136 | else: |
| 137 | # Check if we have at least 2 rolls for this frame |
| 138 | if roll_index + 1 < len(self.rolls): |
| 139 | # Spare |
| 140 | if self.rolls[roll_index] + self.rolls[roll_index + 1] == 10: |
| 141 | # Need at least 1 more roll for bonus |
| 142 | if roll_index + 2 < len(self.rolls): |
| 143 | total_score += 10 + self.rolls[roll_index + 2] |
| 144 | # Open frame |
| 145 | else: |
| 146 | total_score += self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 147 | roll_index += 2 |
| 148 | |
| 149 | # Score 10th frame - just sum all rolls in 10th frame |
| 150 | while roll_index < len(self.rolls): |
| 151 | total_score += self.rolls[roll_index] |
| 152 | roll_index += 1 |
| 153 | |
| 154 | return total_score |