| 1 | 1 | class BowlingGame: |
| 2 | 2 | def __init__(self): |
| 3 | | pass |
| 4 | | |
| 3 | self.rolls = [] |
| 4 | self.current_roll = 0 |
| 5 | |
| 5 | 6 | def roll(self, pins): |
| 6 | | pass |
| 7 | | |
| 7 | # Edge Case: Number of pins must be between 0 and 10 |
| 8 | if pins < 0 or pins > 10: |
| 9 | raise ValueError("Pins must be between 0 and 10") |
| 10 | |
| 11 | # Edge Case: Cannot roll after game is over |
| 12 | if self.is_game_over(): |
| 13 | raise ValueError("Cannot roll after game is over") |
| 14 | |
| 15 | # Check if this roll would make the frame total exceed 10 (except for strikes) |
| 16 | if len(self.rolls) % 2 == 1 and self.rolls[-1] != 10: # Second roll of a frame, and first roll wasn't a strike |
| 17 | if self.rolls[-1] + pins > 10: |
| 18 | raise ValueError("Pin count exceeds pins on the lane") |
| 19 | |
| 20 | self.rolls.append(pins) |
| 21 | self.current_roll += 1 |
| 22 | |
| 8 | 23 | def score(self): |
| 9 | | pass |
| 24 | # Edge Case: Initial game with no rolls should return 0 |
| 25 | if len(self.rolls) == 0: |
| 26 | return 0 |
| 27 | |
| 28 | # Edge Case: Cannot score incomplete game |
| 29 | if not self.is_game_over(): |
| 30 | raise IndexError("Score cannot be taken until the end of the game") |
| 31 | |
| 32 | score = 0 |
| 33 | roll_index = 0 |
| 34 | |
| 35 | for frame in range(10): |
| 36 | # Edge Case: Incomplete game (should not happen due to check above, but added for safety) |
| 37 | if roll_index >= len(self.rolls): |
| 38 | raise IndexError("Incomplete game") |
| 39 | |
| 40 | if self.is_strike(roll_index): # Strike |
| 41 | # Edge Case: Not enough rolls for strike bonus |
| 42 | if roll_index + 2 >= len(self.rolls): |
| 43 | raise IndexError("Incomplete game") |
| 44 | |
| 45 | score += 10 + self.strike_bonus(roll_index) |
| 46 | roll_index += 1 |
| 47 | elif self.is_spare(roll_index): # Spare |
| 48 | # Edge Case: Not enough rolls for spare bonus |
| 49 | if roll_index + 2 >= len(self.rolls): |
| 50 | raise IndexError("Incomplete game") |
| 51 | |
| 52 | score += 10 + self.spare_bonus(roll_index) |
| 53 | roll_index += 2 |
| 54 | else: # Open frame |
| 55 | # Edge Case: Not enough rolls for open frame |
| 56 | if roll_index + 1 >= len(self.rolls): |
| 57 | raise IndexError("Incomplete game") |
| 58 | |
| 59 | score += self.sum_of_balls_in_frame(roll_index) |
| 60 | roll_index += 2 |
| 61 | |
| 62 | return score |
| 63 | |
| 64 | def is_game_over(self): |
| 65 | # A game is over when we have completed 10 frames |
| 66 | # For frames 1-9: each frame takes 1 or 2 rolls |
| 67 | # For frame 10: can take 2 or 3 rolls depending on strike/spare |
| 68 | |
| 69 | if len(self.rolls) < 10: |
| 70 | return False |
| 71 | |
| 72 | frame_count = 0 |
| 73 | roll_index = 0 |
| 74 | |
| 75 | # Count completed frames (1-9) |
| 76 | while frame_count < 9 and roll_index < len(self.rolls): |
| 77 | if self.is_strike(roll_index): |
| 78 | roll_index += 1 |
| 79 | else: |
| 80 | roll_index += 2 |
| 81 | frame_count += 1 |
| 82 | |
| 83 | # If we haven't completed 9 frames, game is not over |
| 84 | if frame_count < 9: |
| 85 | return False |
| 86 | |
| 87 | # Handle 10th frame |
| 88 | if roll_index >= len(self.rolls): |
| 89 | return False |
| 90 | |
| 91 | # Check if first roll of 10th frame is a strike |
| 92 | if self.rolls[roll_index] == 10: # First roll is strike |
| 93 | # Need two more rolls |
| 94 | if roll_index + 2 >= len(self.rolls): |
| 95 | return False |
| 96 | # Game is over after these three rolls |
| 97 | return roll_index + 3 == len(self.rolls) |
| 98 | else: |
| 99 | # Check if second roll exists |
| 100 | if roll_index + 1 >= len(self.rolls): |
| 101 | return False |
| 102 | |
| 103 | # Check if first two rolls make a spare |
| 104 | if self.rolls[roll_index] + self.rolls[roll_index + 1] == 10: # Spare |
| 105 | # Need one more roll |
| 106 | if roll_index + 2 >= len(self.rolls): |
| 107 | return False |
| 108 | # Game is over after these three rolls |
| 109 | return roll_index + 3 == len(self.rolls) |
| 110 | else: # Open frame |
| 111 | # Game is over after these two rolls |
| 112 | return roll_index + 2 == len(self.rolls) |
| 113 | |
| 114 | def is_strike(self, roll_index): |
| 115 | return self.rolls[roll_index] == 10 |
| 116 | |
| 117 | def is_spare(self, roll_index): |
| 118 | return self.rolls[roll_index] + self.rolls[roll_index + 1] == 10 |
| 119 | |
| 120 | def sum_of_balls_in_frame(self, roll_index): |
| 121 | return self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 122 | |
| 123 | def spare_bonus(self, roll_index): |
| 124 | return self.rolls[roll_index + 2] |
| 125 | |
| 126 | def strike_bonus(self, roll_index): |
| 127 | return self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 128 | |
| 129 | # Handled Edge Cases: Number of pins must be between 0 and 10, Cannot roll after game is over, Cannot score incomplete game, Incomplete game, Not enough rolls for strike bonus, Not enough rolls for spare bonus, Not enough rolls for open frame, Too many rolls |