The main idea behind code katas is similar to martial arts katas, meaning they are exercises meant to be repeated in order to become better at certain concepts, motions and so on, until they're so ingrained that they're near instinctual.
I'm not totally sold on the concept when it comes to programming, though that may be short-sighted on my part. While doing this daily or weekly or however frequently I choose to practice it, I'm not sure it will make me much better at anything other than writing bowling scorers. The conceptual leaps necessary to write it are really only made the first time, and I'm not sure how well they translate to other problems. Regularly solving a variety of problems seem like better use of practice time, but as I said, I may be missing something.
I'm also planning on diving into Haskell this week to try a new language that works differently from what I'm used to, and I figured that small standardised exercises would be a good way of illustrating some conceptual differences between languages. So, now that I've done this in C#, look for a version in Haskell in the near future.
As a side note, this solution isn't perfect. There's a temporal dependency in the Score method and adding the first two balls of the current index will happen twice unless you roll a strike. But I didn't see a handy way fixing those without cluttering the code more than I thought it was worth.
For completeness, I made the scorer a private member of the test class, and extracted the ability to roll an arbitrary number of the same number of pins into its own method, as below.
private Scorer scorer = new Scorer(); [SetUp] public void SetUp() { scorer = new Scorer(); } private void RollSamePinsForAllBalls(int pins, int rolls) { for (var i = 0; i < rolls; i++) scorer.Roll(pins); }My first test was for a complete series of gutterballs with an expected final score of zero. I'd like to think that I am better bowler than that, but it seemed a good first case.
[Test] public void Score_AllRollsAreZero_Zero() { RollSamePinsForAllBalls(0, 20); var score = scorer.Score(); Assert.AreEqual(0, score); }Making it pass was simple enough.
public int Score() { return 0; } public void Roll(int roll) { }Next, I figured I'd work on my precision rolling, knocking off only one pin per roll, with a matching test expecting a grand total of twenty points.
[Test] public void Score_AllFramesAreOnes_Twenty() { RollSamePinsForAllBalls(1, 20); var score = scorer.Score(); Assert.AreEqual(20, score); }At this point, the Score method actually had to do some thinking, so I scored without any consideration for strikes and spares. My thought at this stage was that I'd be able to do this with some clever manipulation of the rolls list to figure out strike and spare scoring later.
private readonly IList<int> rolls = new List<int>(); public int Score { var score = 0; foreach(var pins in rolls) score += pins; return score; } public void Roll(int pins) { rolls.Add(pins); }So far so good. The next test case was my dream game, a perfect three hundred, rolling twelve strikes in a row.
[Test] public void Score_AllRollsAreStrikes_ThreeHundred() { RollSamePinsForAllBalls(10, 12); var score = scorer.Score(); Assert.AreEqual(300, score); }This is where I first realized that trying to score a game based on patterns in the list of individual rolls just wasn't going to hold up. I would show the code, but after fixing it, I found myself having a hard time finding back to my original train of thought and reproducing it. Perhaps it suffices to say that it was an ugly exercise in index tracking and list manipulation that even in the end didn't give the right answers.
After a couple of attempts at parsing the list backwards, figuring out which index should be my start index and so on, I realized that a higher level of control was required. I switched over to counting frames and tracking the index based on the first ball of each frame, and the rest came together.
private readonly IList<int> rolls = new List<int>(); public int Score() { var score = 0; var currentFirstBall = 0; for (var frame = 1; frame <= 10; frame++) { if(rolls[currentFirstBall] == 10) { score += rolls[currentFirstBall] + rolls[currentFirstBall + 1] + rolls[currentFirstBall + 2]; currentFirstBall++; } else { score += rolls[currentFirstBall] + rolls[currentFirstBall + 1]; currentFirstBall += 2; } } return score; }It could use some refactoring, but it worked. Adding another case for spares was simple once the pattern was in place. Testing with all fives should give an all spares result.
[Test] public void Score_AllRollsAreSpares_OneHundredFifty() { RollSamePinsForAllBalls(5, 21); var score = scorer.Score(); Assert.AreEqual(150, score); }
private readonly IList<int> rolls = new List<int>(); public int Score() { var score = 0; var currentFirstBall = 0; for (var frame = 1; frame <= 10; frame++) { if(rolls[currentFirstBall] == 10) { score += rolls[currentFirstBall] + rolls[currentFirstBall + 1] + rolls[currentFirstBall + 2]; currentFirstBall++; } else if(rolls[currentFirstBall] + rolls[currenFirstBall + 1] == 10) { score += rolls[currentFirstBall] + rolls[currentFirstBall + 1] + rolls[currentFirstBall + 2]; currentFirstBall += 2; } else { score += rolls[currentFirstBall] + rolls[currentFirstBall + 1]; currentFirstBall += 2; } } return score; }Then I added a final bowled game that was a mix of strikes, spares and normal balls, mostly for my own feeling of security, which passed right away.
[Test] public void Score_RollsAreMixed_OneHundredEightyFour() { scorer.Roll(4); scorer.Roll(5); scorer.Roll(10); scorer.Roll(5); scorer.Roll(5); scorer.Roll(10); scorer.Roll(10); scorer.Roll(10); scorer.Roll(4); scorer.Roll(5); scorer.Roll(0); scorer.Roll(10); scorer.Roll(8); scorer.Roll(1); scorer.Roll(10); scorer.Roll(10); scorer.Roll(6); var score = scorer.Score(); Assert.AreEqual(184, score); }All that remained after that was refactoring out redundancies and hopefully making the code more readable. I've added the complete test and scorer classes below.
using NUnit.Framework; namespace BowlingKata { [TestFixture] public class ScorerTest { private Scorer scorer = new Scorer(); [SetUp] public void SetUp() { scorer = new Scorer(); } private void RollSamePinsForAllBalls(int pins, int rolls) { for (var i = 0; i < rolls; i++) scorer.Roll(pins); } [Test] public void Score_AllRollsAreZero_Zero() { RollSamePinsForAllBalls(0, 20); var score = scorer.Score(); Assert.AreEqual(0, score); } [Test] public void Score_AllFramesAreOnes_Twenty() { RollSamePinsForAllBalls(1, 20); var score = scorer.Score(); Assert.AreEqual(20, score); } [Test] public void Score_AllRollsAreStrikes_ThreeHundred() { RollSamePinsForAllBalls(10, 12); var score = scorer.Score(); Assert.AreEqual(300, score); } [Test] public void Score_AllRollsAreSpares_OneHundredFifty() { RollSamePinsForAllBalls(5, 21); var score = scorer.Score(); Assert.AreEqual(150, score); } [Test] public void Score_RollsAreMixed_OneHundredEightyFour() { scorer.Roll(4); scorer.Roll(5); scorer.Roll(10); scorer.Roll(5); scorer.Roll(5); scorer.Roll(10); scorer.Roll(10); scorer.Roll(10); scorer.Roll(4); scorer.Roll(5); scorer.Roll(0); scorer.Roll(10); scorer.Roll(8); scorer.Roll(1); scorer.Roll(10); scorer.Roll(10); scorer.Roll(6); var score = scorer.Score(); Assert.AreEqual(184, score); } } }
using System.Collections.Generic; namespace BowlingKata { public class Scorer { private readonly IList<int> rolls = new List<int>(); private int score; private int currentFirstBall; public void Roll(int roll) { rolls.Add(roll); } public int Score() { ScoreAllRolls(); return score; } private void ScoreAllRolls() { for (var frame = 1; frame <= 10; frame++) ScoreOneFrame(); } private void ScoreOneFrame() { if (IsStrike()) AddStrike(); else if (IsSpare()) AddSpare(); else AddNormalFrame(); } private bool IsStrike() { return rolls[currentFirstBall] == 10; } private void AddStrike() { score += ScoreRolls(3); currentFirstBall++; } private bool IsSpare() { return ScoreRolls(2) == 10; } private void AddSpare() { score += ScoreRolls(3); currentFirstBall += 2; } private void AddNormalFrame() { score += ScoreRolls(2); currentFirstBall += 2; } private int ScoreRolls(int numberOfRolls) { var sumOfRolls = 0; for (var i = currentFirstBall; i < currentFirstBall + numberOfRolls; i++) sumOfRolls += rolls[i]; return sumOfRolls; } } }
Nice post, Christian! You're definitely getting the hang of TDD.
ReplyDeleteKatas do make sense for software development, much the same way they do for martial arts. What you may find, as you retry the same kata, is that the way you drive to the solution will change.
For instance, you make a pretty big jump in the code under test when moving from rolling all gutter balls to rolling all ones. The test progression is perfect, but you move to adding a collection too soon. All you need to get the "roll all ones" test to pass is to create a new class variable that is an integer, named "score." Then, every time the "Roll" method is called, add pins to score. Finally, the "Score" method contains only one line: return score;
Part of the key to TDD is to do the simplest thing in the code to get the new test, and all previous tests to pass. Why? Because, if I can do something ugly like I mention above, it indicates that I don't have sufficient test coverage and need to add more test cases. So, then I ask "what is the next test I need to write to force my implementation closer to the 'real' implementation?"
There are a couple other jumps, some in the tests, that are too big, but that's good for starters. :)
Keep up the great work!
Hi I found very interesting your contribution to'm bowling novice kata c # you can help me make my point application
ReplyDeleteBowling have a program running on kata bowling or something to guide me to
implementing kata class bowling thank you very much.
Ferrioli@gmail.com
Gustavo, I'm sorry, but I don't quite understand what you're asking. From what I can tell, you need help with a bowling application maybe? If so, what you have here is a good guide to how to do scoring, done step by step, with unit tests. If there's a concept that is unclear, I can try to explain that, but I don't have the time to do software development for others, in general.
ReplyDeleteMuchas gracia por tu respuesta comenzare a intentar armarlo si .
ReplyDelete