Files
python/exercises/bowling/example.py
Marc Chan 518f5e14ec bowling: update tests to v1.2.0 (#1272)
* 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
2018-02-19 11:49:34 -06:00

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)