I've taken a couple of different approaches in the Java version to compare them. In Python my instinct was to take a functional approach: consume a frame, produce a score and the unconsumed frames. Consuming a frame is easy to build up test by test, to move from adding scores to handling the more complex features of spares and strikes. Once a frame can be handled, a whole game is easy.
So, tests first, in the order written.
import unittest import bowlinggame class Test(unittest.TestCase): def testScoreNumberTurn(self): (score, remainder) = bowlinggame.processTurn("12") self.assertEqual(3, score) self.assertEqual(0, len(remainder)) def testScoreNoHit(self): (score, remainder) = bowlinggame.processTurn("--") self.assertEqual(0, score) self.assertEqual(0, len(remainder)) #I'm sufficiently confident of the way scoreNormalRoll is being used not to test "1-" and "-1" etc def testScoreNumberTurnNotLast(self): (score, remainder) = bowlinggame.processTurn("1234") self.assertEqual(3, score) self.assertEqual(2, len(remainder)) def testScoreSpareTurnNotLast(self): (score, remainder) = bowlinggame.processTurn("1/34") self.assertEqual(13, score) self.assertEqual(2, len(remainder)) def testScoreStrikeFollowedByTwoBalls(self): (score, remainder) = bowlinggame.processTurn("X12") self.assertEqual(13, score) self.assertEqual(2, len(remainder)) def testScoreStrikeFollowedBySpare(self): (score, remainder) = bowlinggame.processTurn("X1/") self.assertEqual(20, score) self.assertEqual(2, len(remainder)) def testScoreStrikeFollowedByNumbers(self): (score, remainder) = bowlinggame.processTurn("X12") self.assertEqual(13, score) self.assertEqual(2, len(remainder)) def testScoreStrikeFollowedByStrikeNumbers(self): (score, remainder) = bowlinggame.processTurn("XX23") self.assertEqual(22, score) self.assertEqual(3, len(remainder)) def testScoreStrikeFollowedByStrikes(self): (score, remainder) = bowlinggame.processTurn("XXXX") self.assertEqual(30, score) self.assertEqual(3, len(remainder)) def testSimpleGame(self): score = bowlinggame.processGame("12345123451234512345") self.assertEqual(60, score) def testPerfectGame(self): score = bowlinggame.processGame("XXXXXXXXXXXX") self.assertEqual(300, score) def testHeartbreakGame(self): score = bowlinggame.processGame("9-9-9-9-9-9-9-9-9-9-") self.assertEqual(90, score) def testAllSparesGame(self): score = bowlinggame.processGame("5/5/5/5/5/5/5/5/5/5/5") self.assertEqual(150, score) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()
And the code. The various methods were added to as the tests were produced, with a couple of refactoring passes.
''' Bowling Game Kata Created on 11 May 2015 @author: Dan Chalmers''' def processGame(line): score = 0 for _ in range(0,10): (turnScore, line) = processTurn(line) score += turnScore return score def processTurn(throws): if throws[1] == '/': return (10+scoreExtraBalls(1, throws[2:]), throws[2:]) elif throws[0] == 'X': return (10+scoreExtraBalls(2, throws[1:]), throws[1:]) else: return (scoreNormalRoll(throws[0]) + scoreNormalRoll(throws[1]), throws[2:]) # Strikes and spares get 2 or 1 extra balls, with no further extras, that just score def scoreExtraBalls(extras, throws): if extras == 0: return 0 # A spare extra ball will give 10 overall, needs some special code. # But a strike in the extras works as a number! elif extras == 2 and throws[1] == '/': return 10 else: return scoreNormalRoll(throws[0]) + scoreExtraBalls(extras-1, throws[1:]) def scoreNormalRoll(throw): if throw == '-': return 0 elif throw == 'X': return 10 else: return int(throw)
A version which pre-processes spares to 10-previous (and X to 10 and - to 0) would be almost as fiddly as this, even if the core algorithm is more compact. In this version the connection between the special outcomes and the behaviour is arguably clearer.
I've written using the "consume a turn" approach in Java too, and being able to return a pair rather than making a class to generate Frame objects instantly makes this neater. The Python version has fewer named functions, and these functions are marginally more complex - but, I think, readable entities.
I think that this Kata fits some of Python's strengths neatly: a functional approach without needing to store state, and processing a stream of values (my Phone Numbers solution also played to this).
No comments:
Post a Comment