| 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 | # Edge Case: In any frame except the 10th, a player cannot knock down more than 10 pins |
| 16 | if len(self.rolls) < 18 or (len(self.rolls) == 18 and len(self.rolls) % 2 == 0): |
| 17 | # Not in 10th frame or starting a new throw in 10th frame |
| 18 | if len(self.rolls) % 2 == 1 and self.rolls[-1] + pins > 10: |
| 19 | raise ValueError("Cannot knock down more than 10 pins in a frame") |
| 20 | |
| 21 | # Edge Case: In the 10th frame, special rules apply for fill balls |
| 22 | if len(self.rolls) >= 18: |
| 23 | # We're in the 10th frame |
| 24 | # If it's the third roll, check if previous two were strikes or if second was a spare |
| 25 | if len(self.rolls) == 19: |
| 26 | # Second roll in 10th frame was just made |
| 27 | # If first two rolls were strikes, third is allowed |
| 28 | # If first was strike and second completed a spare, third is allowed |
| 29 | # If first was strike and second was not a strike, sum must not exceed 10 |
| 30 | if self.rolls[18] == 10: # First roll was a strike |
| 31 | if self.rolls[19] != 10 and self.rolls[18] + self.rolls[19] > 10: |
| 32 | raise ValueError("Cannot knock down more than 10 pins in a frame") |
| 33 | else: # First roll was not a strike |
| 34 | if self.rolls[18] + self.rolls[19] > 10: |
| 35 | raise ValueError("Cannot knock down more than 10 pins in a frame") |
| 36 | elif len(self.rolls) == 20: |
| 37 | # Third roll in 10th frame |
| 38 | # If first two were strikes, third can be 0-10 |
| 39 | # If first was strike and second completed spare, third can be 0-10 |
| 40 | # If first was strike and second was not strike, third must not make frame exceed limits |
| 41 | # If first was not strike and second completed spare, third can be 0-10 |
| 42 | if self.rolls[18] != 10 and self.rolls[18] + self.rolls[19] != 10: |
| 43 | # This shouldn't happen as game should be over |
| 44 | raise ValueError("Cannot roll after game is over") |
| 45 | |
| 46 | # If we have a strike in 19th position (second roll), check constraints |
| 47 | if self.rolls[18] == 10 and self.rolls[19] != 10: |
| 48 | if self.rolls[19] + pins > 10: |
| 49 | raise ValueError("Cannot knock down more than 10 pins in a frame") |
| 50 | |
| 51 | self.rolls.append(pins) |
| 52 | |
| 8 | 53 | def score(self): |
| 9 | | pass |
| 54 | # Edge Case: Cannot score incomplete game |
| 55 | if not self.is_game_over(): |
| 56 | raise ValueError("Score cannot be taken until the end of the game") |
| 57 | |
| 58 | score = 0 |
| 59 | roll_index = 0 |
| 60 | |
| 61 | for frame in range(10): |
| 62 | # Edge Case: Strike in regular frames |
| 63 | if self.is_strike(roll_index) and frame < 9: |
| 64 | score += 10 + self.strike_bonus(roll_index) |
| 65 | roll_index += 1 |
| 66 | # Edge Case: Spare in regular frames |
| 67 | elif self.is_spare(roll_index) and frame < 9: |
| 68 | score += 10 + self.spare_bonus(roll_index) |
| 69 | roll_index += 2 |
| 70 | # Edge Case: Open frame in regular frames |
| 71 | elif frame < 9: |
| 72 | score += self.sum_of_balls_in_frame(roll_index) |
| 73 | roll_index += 2 |
| 74 | # Edge Case: 10th frame scoring |
| 75 | else: # 10th frame |
| 76 | score += self.sum_of_balls_in_frame(roll_index) |
| 77 | # If there are more rolls (fill balls), add them |
| 78 | if len(self.rolls) > roll_index + 2: |
| 79 | score += self.rolls[roll_index + 2] |
| 80 | if len(self.rolls) > roll_index + 3: |
| 81 | score += self.rolls[roll_index + 3] |
| 82 | roll_index += 2 |
| 83 | |
| 84 | return score |
| 85 | |
| 86 | def is_game_over(self): |
| 87 | if len(self.rolls) < 18: |
| 88 | return False |
| 89 | |
| 90 | if len(self.rolls) == 18: |
| 91 | # 9 frames completed, 10th frame has 2 rolls |
| 92 | # Game is over if 10th frame is open |
| 93 | return self.rolls[18] + self.rolls[19] < 10 |
| 94 | |
| 95 | if len(self.rolls) == 19: |
| 96 | # 10th frame had a strike or spare, one fill ball thrown |
| 97 | # Game is over if 10th frame was a spare or if it was a strike but not followed by another strike |
| 98 | # that would require a second fill ball |
| 99 | tenth_frame_first = self.rolls[18] |
| 100 | tenth_frame_second = self.rolls[19] |
| 101 | |
| 102 | # If 10th frame was spare, game is over |
| 103 | if tenth_frame_first + tenth_frame_second == 10: |
| 104 | return True |
| 105 | |
| 106 | # If 10th frame was strike, need to check if second fill ball is needed |
| 107 | # Second fill ball is needed only if first fill ball was a strike |
| 108 | return tenth_frame_second != 10 |
| 109 | |
| 110 | if len(self.rolls) >= 20: |
| 111 | return True |
| 112 | |
| 113 | return False |
| 114 | |
| 115 | def is_strike(self, roll_index): |
| 116 | return self.rolls[roll_index] == 10 |
| 117 | |
| 118 | def is_spare(self, roll_index): |
| 119 | return self.rolls[roll_index] + self.rolls[roll_index + 1] == 10 |
| 120 | |
| 121 | def sum_of_balls_in_frame(self, roll_index): |
| 122 | return self.rolls[roll_index] + self.rolls[roll_index + 1] |
| 123 | |
| 124 | def spare_bonus(self, roll_index): |
| 125 | return self.rolls[roll_index + 2] |
| 126 | |
| 127 | def strike_bonus(self, roll_index): |
| 128 | return self.rolls[roll_index + 1] + self.rolls[roll_index + 2] |
| 129 | |
| 130 | # Handled Edge Cases: Number of pins must be between 0 and 10, Cannot roll after game is over, In any frame except the 10th a player cannot knock down more than 10 pins, In the 10th frame special rules apply for fill balls, Cannot score incomplete game, Strike in regular frames, Spare in regular frames, Open frame in regular frames, 10th frame scoring |