diff --git a/config.json b/config.json index edfb8dad84..10fd138251 100644 --- a/config.json +++ b/config.json @@ -80,6 +80,18 @@ "transforming" ] }, + { + "uuid": "ca970fee-71b4-41e1-a5c3-b23bf574eb33", + "slug": "bowling", + "core": false, + "unlocked_by": null, + "difficulty": 5, + "topics": [ + "classes", + "exception_handling", + "logic" + ] + }, { "uuid": "7961c852-c87a-44b0-b152-efea3ac8555c", "slug": "isbn-verifier", diff --git a/exercises/bowling/.gitignore b/exercises/bowling/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exercises/bowling/bowling.py b/exercises/bowling/bowling.py new file mode 100644 index 0000000000..40725d2dcb --- /dev/null +++ b/exercises/bowling/bowling.py @@ -0,0 +1,11 @@ + + +class BowlingGame(object): + def __init__(self): + pass + + def roll(self, pins): + pass + + def score(self): + pass diff --git a/exercises/bowling/bowling_test.py b/exercises/bowling/bowling_test.py new file mode 100644 index 0000000000..ee8e5dab53 --- /dev/null +++ b/exercises/bowling/bowling_test.py @@ -0,0 +1,193 @@ +import unittest + +from bowling import BowlingGame + + +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.1 + +class BowlingTests(unittest.TestCase): + def setUp(self): + self.game = BowlingGame() + + def roll(self, rolls): + [self.game.roll(roll) for roll in rolls] + + def roll_and_score(self, rolls): + self.roll(rolls) + return self.game.score() + + def test_should_be_able_to_score_a_game_with_all_zeros(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 0) + + def test_should_be_able_to_score_a_game_with_no_strikes_or_spares(self): + rolls = [3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 90) + + def test_a_spare_follow_by_zeros_is_worth_ten_points(self): + rolls = [6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 10) + + def test_points_scored_in_the_roll_after_a_spare_are_counted_twice(self): + rolls = [6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 16) + + def test_consecutive_spares_each_get_a_one_roll_bonus(self): + rolls = [5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 31) + + def test_last_frame_spare_gets_bonus_roll_that_is_counted_twice(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 17) + + def test_a_strike_earns_ten_points_in_a_frame_with_a_single_roll(self): + rolls = [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 10) + + def test_two_rolls_points_after_strike_are_counted_twice(self): + rolls = [10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 26) + + def test_consecutive_stikes_each_get_the_two_roll_bonus(self): + rolls = [10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 81) + + def test_strike_in_last_frame_gets_two_roll_bonus_counted_once(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 10, 7, 1] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 18) + + def test_rolling_spare_with_bonus_roll_does_not_get_bonus(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 10, 7, 3] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 20) + + def test_strikes_with_the_two_bonus_rolls_do_not_get_bonus_rolls(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, + 10, 10] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 30) + + def test_strike_with_bonus_after_spare_in_last_frame_gets_no_bonus(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, + 3, 10] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 20) + + def test_all_strikes_is_a_perfect_game(self): + rolls = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 300) + + def test_rolls_cannot_score_negative_points(self): + + self.assertRaises(ValueError, self.game.roll, -11) + + def test_a_roll_cannot_score_more_than_10_points(self): + + self.assertRaises(ValueError, self.game.roll, 11) + + def test_two_rolls_in_a_frame_cannot_score_more_than_10_points(self): + self.game.roll(5) + + self.assertRaises(ValueError, self.game.roll, 6) + + def test_bonus_after_strike_in_last_frame_cannot_score_more_than_10(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10] + + self.roll(rolls) + + self.assertRaises(ValueError, self.game.roll, 11) + + def test_bonus_aft_last_frame_strk_can_be_more_than_10_if_1_is_strk(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, + 10, 6] + + score = self.roll_and_score(rolls) + + self.assertEqual(score, 26) + + def test_bonus_aft_last_frame_strk_cnt_be_strk_if_first_is_not_strk(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 6] + + self.roll(rolls) + + self.assertRaises(ValueError, self.game.roll, 10) + + def test_an_incomplete_game_cannot_be_scored(self): + rolls = [0, 0] + + self.roll(rolls) + + self.assertRaises(IndexError, self.game.score) + + def test_cannot_roll_if_there_are_already_ten_frames(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + + self.roll(rolls) + + self.assertRaises(IndexError, self.game.roll, 0) + + def test_bonus_rolls_for_strike_must_be_rolled_before_score_is_calc(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10] + + self.roll(rolls) + + self.assertRaises(IndexError, self.game.score) + + def test_both_bonuses_for_strike_must_be_rolled_before_score(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10] + + self.roll(rolls) + + self.assertRaises(IndexError, self.game.score) + + def test_bonus_rolls_for_spare_must_be_rolled_before_score_is_calc(self): + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3] + + self.roll(rolls) + + self.assertRaises(IndexError, self.game.score) + + +if __name__ == '__main__': + unittest.main() diff --git a/exercises/bowling/example.py b/exercises/bowling/example.py new file mode 100644 index 0000000000..81b0551509 --- /dev/null +++ b/exercises/bowling/example.py @@ -0,0 +1,149 @@ +MAX_PINS = 10 +NUM_FRAMES = 10 + + +class BowlingGame(object): + """This class manages the Bowling Game including the roll and score + methods""" + def __init__(self): + self.rolls = [] + self.totalScore = 0 + self.currentFrame = Frame() + self.bonusRollsAccrued = 0 + self.bonusRollsSeen = 0 + + def roll(self, pins): + if self.isBonusRoll(): + self.bonusRollsSeen += 1 + + # is the second roll valid based off the first? + if (self.currentFrame.isOpen() and + self.currentFrame.getFrame()[0] is not None): + if self.currentFrame.getFrame()[0] + pins > MAX_PINS: + raise ValueError("This roll will cause the current frame " + "to be getter than the max number of pins") + + # open a new frame if the last one has been closed + if not self.currentFrame.isOpen(): + self.currentFrame = Frame() + + # valid roll between 0-10 + if pins in range(MAX_PINS + 1): + # raise an error if the game is over and they try to roll again + if ((len(self.rolls) == NUM_FRAMES) and + self.bonusRollsAccrued == 0): + raise IndexError("Max Frames have been reached. Too many " + "rolls") + else: + self.currentFrame.roll(pins, + self.isBonusRoll(), + self.bonusRollsAccrued, + self.bonusRollsSeen) + # if we closed it add it to our rolls + if not self.currentFrame.isOpen(): + self.rolls.append(self.currentFrame) + # If this is the last frame did we earn any bonus rolls? + if len(self.rolls) == NUM_FRAMES: + self.bonusRollsEarned() + else: + raise ValueError("Amount of pins rolled is greater than the max " + "number of pins") + + def score(self): + frame_index = 0 + + while (frame_index <= NUM_FRAMES-1): + frame = self.rolls[frame_index].getFrame() + + roll1 = frame[0] + roll2 = frame[1] + + if self.isStrike(roll1): + self.totalScore += roll1 + self.stikeBonus(frame_index) + else: + if self.isSpare(roll1, roll2): + self.totalScore += roll1 + roll2 + \ + self.spareBonus(frame_index) + else: + self.totalScore += roll1 + roll2 + + frame_index += 1 + + return self.totalScore + + def isStrike(self, pins): + return True if pins == MAX_PINS else False + + def isSpare(self, roll1, roll2): + return True if roll1 + roll2 == MAX_PINS else False + + def stikeBonus(self, frame_index): + bonusroll1 = self.rolls[frame_index+1].getFrame()[0] + bonusroll2 = 0 + # need to go further out if the next on is a strike + if bonusroll1 == 10: + bonusroll2 = self.rolls[frame_index+2].getFrame()[0] + else: + bonusroll2 = self.rolls[frame_index+1].getFrame()[1] + # edge case - if the last roll is a stike the bonus rolls needs to be + # validated + if (not self.isStrike(bonusroll1) and + (bonusroll1 + bonusroll2 > MAX_PINS)): + raise ValueError("The bonus rolls total to greater than the max " + "number of pins") + else: + return bonusroll1 + bonusroll2 + + def spareBonus(self, frame_index): + return self.rolls[frame_index+1].getFrame()[0] + + def isLastFrame(self, frame_index): + return True if frame_index >= len(self.rolls)-1 else False + + def bonusRollsEarned(self): + if len(self.rolls) == NUM_FRAMES: + lastFrame = self.rolls[NUM_FRAMES-1].getFrame() + if self.isStrike(lastFrame[0]): + self.bonusRollsAccrued = 2 + elif self.isSpare(lastFrame[0], lastFrame[1]): + self.bonusRollsAccrued = 1 + else: + self.bonusRollsAccrued = 0 + return + + def isBonusRoll(self): + # if we've already seen all + return True if len(self.rolls) >= NUM_FRAMES else False + + +class Frame(object): + """This class is for internal use only. It divides up the array of + rolls into Frame objects""" + def __init__(self): + self.rolls = [None, None] + self.open = True + + def roll(self, roll, bonusRoll, accruedBonuses, seenBonuses): + # if it's a strike we close the frame + if roll == 10: + self.rolls[0] = 10 + self.rolls[1] = 0 + self.open = False + else: + # first roll, but frame is still open + if self.rolls[0] is None: + self.rolls[0] = roll + # may need to close bonus roll frames before 2 have been seen + if bonusRoll and seenBonuses == accruedBonuses: + self.rolls[1] = 0 + self.open = False + else: + # second roll, closes frame + self.rolls[1] = roll + self.open = False + + def isOpen(self): + return self.open + + def getFrame(self): + return self.rolls