Scala Bowling Kata - still in the middle I suppose 90
"One more roll on a game with 20 rolls and an open 10th frame" should {
20 times { roll(1) }
roll(1) must throwA[IllegalArgumentException]
}
"Two more rolls a game with 10 spares" should {
10 times { spare }
roll(1)
roll(1) must throwA[IllegalArgumentException]
}
"Two marks in the 10th frame should" should {
18 times { roll(1) }
strike
spare
roll(1) must throwA[IllegalArgumentException]
}
On my flight from DFW to SNA, I got these behaviors implemented. The code was pretty ugly!
However, ugly code in hand, passing examples, a slight understanding of some of the problems with my code and a desire to make the BowlingScorer immutable was all I needed to make progress.
I removed the index instance field by rewriting the score method and injecting a tuple into foldLeft (written here using the short-hand notation /:): def scoreAt(frame:Int) =
((0,0) /: (1 to frame)) { (t, _) =>
(t._1 + scoreAtIndex(t._2), t._2 + incrementAt(t._2))
}._1
def onFirstThrow = {
var index = 0
while(index < rolls.length)
if(isStrike(index)) index += 1 else index += 2
index == rolls.length
}
While I am happy I was able to remove the index variable, which was really a parameter being passed around in a field (ugly), I am not happy with this method.
I changed the roll method to return a new instance of a BowlingScorer, making the bowling scorer immutable: def roll(roll:Int) = {
validate(roll)
new BowlingScorer(rolls ++ Array(roll))
}
So I think I’m still somewhere in the middle of working through this code. Again, I’m still learning Scala. I have a lot to learn. I really only barely understand functional programming and, frankly, the Eclipse IDE, while functional, is getting in the way quite a bit. So for a toy example it is OK. Given the choice of this environment or vi and the command line, I’d not pick the former. (I might give the other IDE’s a go, but that’s not really what I’m interested in learning right now.)
So here’s the next version. I plan to work through all of the comments I’ve yet to process from the previous blog posting over the next few days. If you can recommend a better implementation of onFirstThrow, I’d appreciate it.
Other general comments also welcome.
BowlingScorerExampleGroup.scala
package com.om.example
import org.specs._
object BowlingScorerExampleGroup extends SpecificationWithJUnit {
var scorer = new BowlingScorer(Nil);
def roll(value:Int) =
scorer = scorer.roll(value)
def haveAScoreOf(expected:Int) =
scorer.score must_== expected
def strike =
roll(10)
def spare {
roll(5)
roll(5)
}
implicit def intToDo(count: Int) = {
new {
def times(f: => Unit) = {
1 to count foreach { _ => f }
}
}
}
"A Newly Created Bowling Scorer" should {
haveAScoreOf(0)
}
"A game with all 0's" should {
20 times { roll(0) }
haveAScoreOf(0)
}
"A game with all 1's" should {
20 times { roll(1) }
haveAScoreOf(20)
}
"A game with a single spare followed by a 5" should {
spare
roll(5)
haveAScoreOf(20)
}
"A game with all 5's" should {
10 times { spare }
roll(5)
haveAScoreOf(150)
}
"A game with a single strike followed by a 4" should {
strike
roll(4)
haveAScoreOf(18)
}
"A game with a strike, spare then an open frame with two 3's" should {
strike
spare
2 times { roll(3) }
haveAScoreOf(39)
}
"A game with strike, spare then an open frame with two 3's" should {
spare
strike
2 times { roll(3) }
haveAScoreOf(42)
}
"A Dutch 200 game, Spare-Strike" should {
5 times {
spare
strike
}
spare
haveAScoreOf(200)
}
"A Dutch 200 game, Strike-Spare" should {
5 times {
strike
spare
}
strike
haveAScoreOf(200)
}
"A Perfect game" should {
12 times { strike }
haveAScoreOf(300)
}
"The score for each frame of a Perfect game, each frame" should {
12 times { strike }
1 to 10 foreach { frame => scorer.scoreAt(frame) must_== 30 * frame }
}
"An individaul roll of > 10" should {
roll(11) must throwA[IllegalArgumentException]
}
"An iniviaul roll of < 0" should {
roll(-1) must throwA[IllegalArgumentException]
}
"A frame trying to contain more than 10 pins" should {
roll(8)
roll(3) must throwA[IllegalArgumentException]
}
"One more roll on a game with 20 rolls and an open 10th frame" should {
20 times { roll(1) }
roll(1) must throwA[IllegalArgumentException]
}
"Two more rolls a game with 10 spares" should {
10 times { spare }
roll(1)
roll(1) must throwA[IllegalArgumentException]
}
"Two marks in the 10th frame should" should {
18 times { roll(1) }
strike
spare
roll(1) must throwA[IllegalArgumentException]
}
}
BowlingScorer.scala
package com.om.example
class BowlingScorer(rollsSoFar:List[Int]){
val rolls:List[Int] = rollsSoFar
def roll(roll:Int) = {
validate(roll)
new BowlingScorer(rolls ++ Array(roll))
}
def validate(roll:Int) {
if(invalidRoll(roll))
throw new IllegalArgumentException("Individaul rolls must be from 0 .. 10")
if(frameRollTooHigh(roll))
throw new IllegalArgumentException("Total of rolls for frame must not exceed 10");
if(willBeTooManyRolls)
throw new IllegalArgumentException("Game over, no more rolls allowed")
}
def invalidRoll(roll:Int) =
(0 to 10 contains(roll)) == false
def frameRollTooHigh(roll:Int) =
openScoreAt(indexToValidate) + roll > 10
def willBeTooManyRolls =
tenthRolled(indexOf10thFrame) && nextRollTooMany(indexOf10thFrame)
def tenthRolled(tenthIndex:Int) =
tenthIndex < rolls.length
def nextRollTooMany(tenthIndex: Int) =
allowedTenthFrameRolls(tenthIndex) < rollsInTenthFrame(tenthIndex) + 1
def indexOf10thFrame =
(0 /: (1 until 10)) {(c, _) => c + incrementAt(c)}
def allowedTenthFrameRolls(index:Int) =
if(isMark(index)) 3 else 2
def rollsInTenthFrame(index: Int) =
rolls.length - index
def indexToValidate =
if(onFirstThrow) rolls.length else rolls.length - 1
def onFirstThrow = {
var index = 0
while(index < rolls.length)
if(isStrike(index)) index += 1 else index += 2
index == rolls.length
}
def scoreAt(frame:Int) =
((0,0) /: (1 to frame)) { (t, _) =>
(t._1 + scoreAtIndex(t._2), t._2 + incrementAt(t._2))
}._1
def score = scoreAt(10)
def scoreAtIndex(index:Int) =
if(isMark(index)) markScoreAt(index) else openScoreAt(index)
def incrementAt(index:Int) =
if(isStrike(index)) 1 else 2
def isMark(index:Int) =
isStrike(index) || isSpare(index)
def isStrike(index:Int) =
valueAt(index) == 10
def markScoreAt(index:Int) =
sumNext(index, 3)
def isSpare(index:Int) =
openScoreAt(index) == 10 && valueAt(index) != 10
def openScoreAt(index:Int) =
sumNext(index, 2)
def sumNext(index:Int, count:Int) =
(0 /: (index until index+count))(_ + valueAt(_))
def valueAt(index:Int) =
if(rolls.length > index) rolls(index) else 0
}
Scala Bowling Kata - somewhere in the middle... 47
- Installed the Eclipse Scala Plugin
- Installed Scala using Mac Ports
- Figured out how to get those things playing nice (the plugin page pretty much did that, but in a nutshell, add a few jar files to the classpath)
- We don’t need YADT – Yet another damn term
- Trait is a heavily overloaded word
- I like the term BDD better and it fits.
Anyway, one such choice was Specs, which is what I decided to use.
So back to yak shaving:- I added another jar to my classpath in Eclipse
- Then read how to get it running in Eclipse. Not too bad, I suppose.
So now I need to learn Scala. Sure, I’ve used it, but far less than Ruby. So it took me several hours to get specs running along with writing some Scala code to score a game – I’m glad I know the domain at least.
I wanted to make similar behaviors to the ones I wrote for the Ruby version, which I did.
However, unlike the Ruby version, I was curious what would happen if I:- Took an approach similar to Uncle Bob – strikes take one slot in an array
- Added input validation
On the one hand, there are some interesting things I managed to create. On the other hand, I’ve got a bit of a mess. I have a stateful object to avoid passing parameters so that I can write some of the code cleanly. I know I need to add in an intermediate computational object, and I’m going to get to that. However, I wanted to get feedback on what I’ve put out there so far.
Specifically,- What do you think of the (bdd-style) examples from specc?
- What is the correct way to write the Times(20).Do( ...) thing I came up with, there has be a better way?
- For the part of the bowling scoring code that is not stateful (read this as, does not violate the SRP), what do you think of it?
- How would you remove most/all of the state (other than the individual rolls) out of the Bowling scorer class? (Or would you choose to have the roll() method return a new instance of BowlingScorer with the new score recorded?)
- Notice that the class maintains a mini state machine in the form of tracking whether the first ball of he current frame (not tracked) has or has not been thrown. That’s only there to be able to perform input validation. I considered:
- Walking the array
- Going to 2 slots for every frame (making it easy to find the frame)
- Storing a frame object (ok, I didn’t really consider it, but I did think about it)
- The mini state machine
- nextFrameScore uses the index instance variable, and changes it. This both violates command-query separation and demonstrates a violation of the SRP, but it made the scoreAt method look nice.
An interesting side effect is that scoring marks (strikes and spares) uses the same approach, sum up three rolls total.
I know this needs work. What I’ve got works according to its current specification (its examples), so in a sense, that’s a good thing because I’ve already stared experimenting with trying out different solutions. However, I am painfully aware of how unaware I am of Scala at the moment, so your (hopefully gentle) feedback will tell me what I need to learn next.
Looking forward to the virtual beating …
Brett
Here are the two files I’ve created so far (and to be clear, all of the examples pass): BowlingScorerExampleGroup.scalapackage com.om.example
import org.specs._
object BowlingScorerExampleGroup extends SpecificationWithJUnit {
var scorer = new BowlingScorer();
def roll(value:Int) {
scorer.roll(value)
}
def rollMany(rolls:Int, value:Int) {
0.until(rolls).foreach { arg => scorer.roll(value) }
}
def haveAScoreOf(expected:Int) {
scorer.score must_== expected
}
def strike {
roll(10)
}
def spare {
rollMany(2, 5)
}
abstract class IDo {
def Do(block: => Unit)
}
def Times(count:Int): IDo = {
return new IDo {
def Do(block: => Unit) {
1.to(count).foreach( arg => block )
}
}
}
"A Newly Created Bowling Scorer" should {
haveAScoreOf(0)
}
"A game with all 0's" should {
Times(20).Do( roll(0) )
haveAScoreOf(0)
}
"A game with all 1's" should {
Times(20).Do { roll(1) }
haveAScoreOf(20)
}
"A game with a single spare followed by a 5" should {
spare
roll(5)
haveAScoreOf(20)
}
"A game with all 5's" should {
Times(10).Do( spare )
roll(5)
haveAScoreOf(150)
}
"A game with a single strike followed by a 4" should {
strike
roll(4)
haveAScoreOf(18)
}
"A game with a strike, spare then an open frame with two 3's" should {
strike
spare
Times(2).Do( roll(3) )
haveAScoreOf(39)
}
"A game with strike, spare then an open frame with two 3's" should {
spare
strike
Times(2).Do( roll(3) )
haveAScoreOf(42)
}
"A Dutch 200 game, Spare-Strike" should {
Times(5).Do {
spare
strike
}
spare
haveAScoreOf(200)
}
"A Dutch 200 game, Strike-Spare" should {
Times(5).Do {
strike
spare
}
strike
haveAScoreOf(200)
}
"A Perfect game" should {
Times(12).Do( strike )
haveAScoreOf(300)
}
"The score for each frame of a Perfect game, each frame" should {
Times(12).Do( strike )
1.to(10).foreach{ frame => scorer.scoreAt(frame) must_== 30 * frame }
}
"An individaul roll of > 10" should {
roll(11) must throwA[IllegalArgumentException]
}
"An iniviaul roll of < 0" should {
roll(-1) must throwA[IllegalArgumentException]
}
"A frame trying to contain more than 10 pins" should {
roll(8)
roll(3) must throwA[IllegalArgumentException]
}
}
BowlingScorer.scala
package com.om.example
class BowlingScorer {
var rolls:Array[Int] = Array()
var index:Int = 0
var firstBallInFrameThrown: Boolean = false;
def roll(roll:Int) = {
validate(roll)
record(roll)
}
def validate(roll:Int) {
if((0).to(10).contains(roll) == false)
throw new IllegalArgumentException("Individaul rolls must be from 0 .. 10")
if(openScoreAt(indexToValidate) + roll > 10)
throw new IllegalArgumentException("Total of rolls for frame must not exceed 10");
}
def record(roll: Int) {
rolls = rolls ++ Array(roll)
firstBallInFrameThrown = firstBallInFrameThrown == false && roll != 10
}
def indexToValidate = {
if(firstBallInFrameThrown) rolls.length - 1 else rolls.length
}
def scoreAt(frame:Int) = {
1.to(frame).foldLeft(0) { (total, frame) => total + nextFrameScore }
}
def score = {
scoreAt(10)
}
def nextFrameScore = {
var result = 0;
if(isStrike(index)) {
result += markScoreAt(index)
index += 1
} else if(isSpare(index)) {
result += markScoreAt(index);
index += 2
} else {
result += openScoreAt(index);
index += 2
}
result
}
def isStrike(index:Int) = {
valueAt(index) == 10
}
def markScoreAt(index:Int) = {
sumNext(index, 3)
}
def isSpare(index:Int) = {
openScoreAt(index) == 10
}
def openScoreAt(index:Int) = {
sumNext(index, 2)
}
def sumNext(index:Int, count:Int) = {
index.until(index+count).foldLeft(0)(_ + valueAt(_))
}
def valueAt(index:Int) = {
if(rolls.length > index) rolls.apply(index) else 0
}
}