Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Implement exercise bowling #790

Merged
merged 30 commits into from
Jan 23, 2018
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c382caa
Implement exercise bowling
thecouchcoder Oct 10, 2017
ad21e2a
bowling: created the files
thecouchcoder Oct 10, 2017
1e16f9e
bowling: implemented test for an all 0 game
thecouchcoder Oct 11, 2017
5b29088
bowling: passing all zeros test
thecouchcoder Oct 11, 2017
028ce96
bowling tested and implemented logic for spares
thecouchcoder Oct 11, 2017
5b61ca4
bowling: bonus roll implemented
thecouchcoder Oct 11, 2017
4527240
bowling: heavily refactored code. Now passing 9 tests
thecouchcoder Oct 11, 2017
1785b1f
bowling: tested and implemeneted first 14 tests
thecouchcoder Oct 12, 2017
73c311f
fix travis issues with line length
thecouchcoder Oct 12, 2017
3eb75d1
bowling: setup config
thecouchcoder Oct 12, 2017
9fb30a7
bowling: passing 19 tests
thecouchcoder Oct 13, 2017
090300d
bowling: fixed error in config.json
thecouchcoder Oct 13, 2017
ff70fd5
bowling: heavily refactoring code to implement frame logic during rol…
thecouchcoder Oct 13, 2017
293bf44
bowling: still alot of refactoring going on and alot is broken
thecouchcoder Oct 13, 2017
0cd0fc1
bowling: still refactoring and still alot of errors, but closer
thecouchcoder Oct 13, 2017
3f1777e
bowling: nearly done refactoring
thecouchcoder Oct 13, 2017
55c3c6c
bowling: finally back to passing all tests like before refactor
thecouchcoder Oct 14, 2017
53b6e52
bowling: added a few more tests and cleaned up after refactor
thecouchcoder Oct 14, 2017
be6cf1c
bowling: passes all tests
thecouchcoder Oct 14, 2017
91d1cf0
bowling: fixing travis issue
thecouchcoder Oct 14, 2017
6034a95
bowling: fixes as requested
thecouchcoder Nov 29, 2017
511732a
bowling: fixes as requested
thecouchcoder Nov 29, 2017
5298cbb
bowling: Travis fixes
thecouchcoder Nov 29, 2017
d3c349d
bowling: Travis fixes
thecouchcoder Nov 29, 2017
833dce2
bowling: Travis fixes
thecouchcoder Nov 29, 2017
1cf050f
bowling: Travis fixes
thecouchcoder Nov 29, 2017
5f8b980
bowling: fixes as requested
thecouchcoder Dec 1, 2017
dca0f0e
Merge branch 'master' into bowling
cmccandless Jan 15, 2018
95b7666
Merge branch 'master' into bowling
cmccandless Jan 19, 2018
de5a2bb
Merge branch 'master' into bowling
cmccandless Jan 23, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@
"transforming"
]
},
{
"uuid": "ca970fee-71b4-41e1-a5c3-b23bf574eb33",
"slug": "bowling",
"core": false,
"unlocked_by": null,
"difficulty": 5,
"topics": [
"classes",
"exception_handling",
"logic"
]
},
{
"uuid": "8648fa0c-d85f-471b-a3ae-0f8c05222c89",
"slug": "hamming",
Expand Down
Empty file added exercises/bowling/.gitignore
Empty file.
11 changes: 11 additions & 0 deletions exercises/bowling/bowling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@


class BowlingGame(object):
def __init__(self):
pass

def roll(self, pins):
pass

def score(self):
pass
192 changes: 192 additions & 0 deletions exercises/bowling/bowling_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with the other exercises, could you please move this line like so:

from bowling import BowlingGame


# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.1

class BowlingTests(unittest.TestCase):
...

Please note the number of blank lines before and after.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

import unittest

from bowling import BowlingGame


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please also leave a comment stating what version of canonical-data.json the tests were adopted as discussed in #784 if aplicable?
The format is:

# Tests adapted from `problem-specifications//canonical-data.json` @ v1.0.0

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()
149 changes: 149 additions & 0 deletions exercises/bowling/example.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring here would be nice 📓

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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't look right, method name doesn't correspond to its actual function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean on this one. you call isOpen to find out if the frame is open (true) or not (false)

return self.open

def getFrame(self):
return self.rolls