* bowling: update tests to v1.2.0 * bowling: remove top blank lines in stub file * bowling: rewrite example to pass tests * bowling: use context manager form for assertRaises
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
MAX_FRAME = 10
|
|
|
|
|
|
class Frame(object):
|
|
def __init__(self, idx):
|
|
self.idx = idx
|
|
self.throws = []
|
|
|
|
@property
|
|
def total_pins(self):
|
|
"""Total pins knocked down in a frame."""
|
|
return sum(self.throws)
|
|
|
|
def is_strike(self):
|
|
return self.total_pins == 10 and len(self.throws) == 1
|
|
|
|
def is_spare(self):
|
|
return self.total_pins == 10 and len(self.throws) == 2
|
|
|
|
def is_open(self):
|
|
return self.total_pins < 10 and len(self.throws) == 2
|
|
|
|
def is_closed(self):
|
|
"""Return whether a frame is over."""
|
|
return self.total_pins == 10 or len(self.throws) == 2
|
|
|
|
def throw(self, pins):
|
|
if self.total_pins + pins > 10:
|
|
raise ValueError("a frame's rolls cannot exceed 10")
|
|
self.throws.append(pins)
|
|
|
|
def score(self, next_throws):
|
|
result = self.total_pins
|
|
if self.is_strike():
|
|
result += sum(next_throws[:2])
|
|
elif self.is_spare():
|
|
result += sum(next_throws[:1])
|
|
return result
|
|
|
|
|
|
class BowlingGame(object):
|
|
def __init__(self):
|
|
self.current_frame_idx = 0
|
|
self.bonus_throws = []
|
|
self.frames = [Frame(idx) for idx in range(MAX_FRAME)]
|
|
|
|
@property
|
|
def current_frame(self):
|
|
return self.frames[self.current_frame_idx]
|
|
|
|
def next_throws(self, frame_idx):
|
|
"""Return a frame's next throws in the form of a list."""
|
|
throws = []
|
|
for idx in range(frame_idx + 1, MAX_FRAME):
|
|
throws.extend(self.frames[idx].throws)
|
|
throws.extend(self.bonus_throws)
|
|
return throws
|
|
|
|
def roll_bonus(self, pins):
|
|
tenth_frame = self.frames[-1]
|
|
if tenth_frame.is_open():
|
|
raise IndexError("cannot throw bonus with an open tenth frame")
|
|
|
|
self.bonus_throws.append(pins)
|
|
|
|
# Check against invalid fill balls, e.g. [3, 10]
|
|
if (len(self.bonus_throws) == 2 and self.bonus_throws[0] != 10 and
|
|
sum(self.bonus_throws) > 10):
|
|
raise ValueError("invalid fill balls")
|
|
|
|
# Check if there are more bonuses than it should be
|
|
if tenth_frame.is_strike() and len(self.bonus_throws) > 2:
|
|
raise IndexError(
|
|
"wrong number of fill balls when the tenth frame is a strike")
|
|
elif tenth_frame.is_spare() and len(self.bonus_throws) > 1:
|
|
raise IndexError(
|
|
"wrong number of fill balls when the tenth frame is a spare")
|
|
|
|
def roll(self, pins):
|
|
if not 0 <= pins <= 10:
|
|
raise ValueError("invalid pins")
|
|
elif self.current_frame_idx == MAX_FRAME:
|
|
self.roll_bonus(pins)
|
|
else:
|
|
self.current_frame.throw(pins)
|
|
if self.current_frame.is_closed():
|
|
self.current_frame_idx += 1
|
|
|
|
def score(self):
|
|
if self.current_frame_idx < MAX_FRAME:
|
|
raise IndexError("frame less than 10")
|
|
if self.frames[-1].is_spare() and len(self.bonus_throws) != 1:
|
|
raise IndexError(
|
|
"one bonus must be rolled when the tenth frame is spare")
|
|
if self.frames[-1].is_strike() and len(self.bonus_throws) != 2:
|
|
raise IndexError(
|
|
"two bonuses must be rolled when the tenth frame is strike")
|
|
return sum(frame.score(self.next_throws(frame.idx))
|
|
for frame in self.frames)
|