Scala Tutorial Through Katas: Bowling Game (Medium)

A programming kata is an exercise which helps a programmer hone his skills through practice and repetition.

This article is part of the series “Scala Tutorial Through Katas”. Articles are divided into easy, medium and hard. Beginners should start with easy ones and move towards more complicated once they feel more comfortable programming in Scala.

For the complete list of Scala katas and solutions please visit the index page

The article assumes that the reader is familiar with the basic usage of ScalaTest asserts and knows how to run them from his favorite Scala IDE (ours is IntelliJ IDEA with the Scala plugin).

Tests that prove that the solution is correct are displayed below. Recommended way to solve this kata is to write the implementation for the first test, confirm that it passes and move to the next. Once all of the tests pass, the kata can be considered solved.

One possible solution is provided below the tests. Try to solve the kata by yourself first.

Bowling Game

Count and sum the scores of a bowling game of one player.

Original description by Martin Fowler: http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata
Wikipedia article with scoring rules: http://en.wikipedia.org/wiki/Ten-pin_bowling#Scoring

Following is the BDD scenario that should be used as the acceptance criteria.

[BDD SCENARIO]

class BowlingGameScenarioTest extends FeatureSpec with GivenWhenThen with Matchers {

  scenario("Bowling game") {

    Given("new game started")
    val game = new BowlingGame

    When("1 pin is knocked in frame 1 and roll 1")
    game.roll(1)

    When("4 pins are knocked in frame 1 and roll 2")
    game.roll(4)

    Then("score is 5")
    game.score should be (5)

    When("4 pins are knocked in frame 2 and roll 1")
    game.roll(4)

    When("5 pins are knocked in frame 2 and roll 2")
    game.roll(5)

    Then("score is 14")
    game.score should be (14)

    When("6 pins are knocked in frame 3 and roll 1")
    game.roll(6)

    When("4 pins are knocked (spare) in frame 3 and roll 2")
    game.roll(4)

    Then("score is 24 (spare roll was still not played")
    game.score should be (24)

    When("5 pins are knocked in frame 4 and roll 1")
    game.roll(5)

    When("5 pins are knocked (spare) in frame 4 and roll 2")
    game.roll(5)

    Then("score is 39 (added 5 to the spare from the frame 3)")
    game.score should be (39)

    When("10 pins are knocked (strike) in frame 5 and roll 1")
    game.roll(10)

    Then("score is 59 (added 10 to the spare from the frame 4; strike roles were still not played)")
    game.score should be (59)

    When("0 pins are knocked in frame 6 and roll 1")
    game.roll(0)

    When("1 pin is knocked in frame 6 and roll 2")
    game.roll(1)

    Then("score is 61 (added 1 to the strike from the frame 5)")
    game.score should be (61)

    When("7 pins are knocked in frame 7 and roll 1")
    game.roll(7)

    When("3 pins are knocked (spare) in frame 7 and roll 2")
    game.roll(3)

    Then("score is 71 (spare roll was still not played)")
    game.score should be (71)

    When("6 pins are knocked in frame 8 and roll 1")
    game.roll(6)

    When("4 pins are knocked (spare) in frame 8 and roll 2")
    game.roll(4)

    Then("score is 87 (added 6 to the spare from the frame 7; spare roll was still not played)")
    game.score should be (87)

    When("10 pins are knocked (strike) in frame 9 and roll 1")
    game.roll(10)

    Then("score is 107 (added 10 to the spare from the frame 8; strike rolls were still not played)")
    game.score should be (107)

    When("2 pins are knocked in frame 10 and roll 1")
    game.roll(2)

    When("8 pins are knocked (spare) in frame 10 and roll 2")
    game.roll(8)

    Then("score is 127 (added 10 to the strike from the frame 9; spare roll was still not played)")
    game.score should be (127)

    When("6 pins are knocked (spare) in the bonus frame")
    game.roll(6)

    Then("score is 133 (added 6 to the spare from the frame 10)")
    game.score should be (133)

  }

Following unit tests were used in a TDD fashion while developing the solution.

[UNIT TESTS]

class BowlingGameUnitTest extends FlatSpec with Matchers {

  "First roll" should "store pins as roll1 and () as roll2" in {
    val game = new BowlingGame
    game.roll(4)
    game.frames(0).roll1 should be (4)
  }

  "Second roll" should "set pins as roll2 to the last rolls element" in {
    val game = new BowlingGame
    game.roll(4)
    game.roll(2)
    game.frames(0).roll1 should be (4)
    game.frames(0).roll2 should be (2)
  }

  "Four roles" should "be stored as two frame elements" in {
    val game = new BowlingGame
    game.roll(1)
    game.roll(2)
    game.roll(3)
    game.roll(4)
    game.frames(0).roll1 should be (1)
    game.frames(0).roll2 should be (2)
    game.frames(1).roll1 should be (3)
    game.frames(1).roll2 should be (4)
  }

  "Score" should "be sum of all roles" in {
    val game = new BowlingGame
    game.roll(1)
    game.roll(2)
    game.roll(3)
    game.roll(4)
    game.roll(5)
    game.roll(4)
    game.score should be (1 + 2 + 3 + 4 + 5 + 4)
  }

  "Strike" should "set 0 as roll2" in {
    val game = new BowlingGame
    game.roll(10)
    game.frames(0).roll1 should be (10)
  }

  it should "add next two rolls (not strikes) to the score" in {
    val game = new BowlingGame
    game.roll(10)
    game.roll(1)
    game.roll(2)
    game.score should be ((10 + 1 + 2) + (1 + 2))
  }

  it should "add next two rolls (strike and not strike) to the score" in {
    val game = new BowlingGame
    game.roll(10)
    game.roll(10)
    game.roll(1)
    game.roll(2)
    game.roll(3)
    game.roll(4)
    game.score should be ((10 + 10 + 1) + (10 + 1 + 2) + (1 + 2) + (3 + 4))
  }

  it should "add next two rolls (both strikes) to the score" in {
    val game = new BowlingGame
    game.roll(10)
    game.roll(10)
    game.roll(10)
    game.roll(2)
    game.roll(3)
    game.score should be ((10 + 10 + 10) + (10 + 10 + 2) + (10 + 2 + 3) + (2 + 3))
  }

  it should "not add additional rolls if they were not played" in {
    val game = new BowlingGame
    game.roll(1)
    game.roll(1)
    game.roll(10)
    game.score should be ((1 + 1) + 10)
  }

  "Spare" should "add next roll to the score" in {
    val game = new BowlingGame
    game.roll(9)
    game.roll(1)
    game.roll(7)
    game.roll(3)
    game.roll(1)
    game.roll(2)
    game.score should be ((9 + 1 + 7) + (7 + 3 + 1) + (1 + 2))
  }

  it should "not add additional roll if it was not played" in {
    val game = new BowlingGame
    game.roll(1)
    game.roll(1)
    game.roll(8)
    game.roll(2)
    game.score should be ((1 + 1) + (8 + 2))
  }

  "Tenth frame" should "give 30 points for three strikes" in {
    val game = new BowlingGame
    (1 to 18).foreach(i => game.roll(0))
    game.roll(10)
    game.roll(10)
    game.roll(10)
    game.score should be (30)
  }

  it should "add one roll for spare" in {
    val game = new BowlingGame
    (1 to 18).foreach(i => game.roll(0))
    game.roll(9)
    game.roll(1)
    game.roll(10)
    game.score should be (20)
  }

  "Perfect game" should "score 300 for 12 strikes (12 regular and 2 bonus)" in {
    val game = new BowlingGame
    (1 to 12).foreach(i => game.roll(10))
    game.score should be (300)
  }

}

Test code can be found in the GitHub BowlingGame.scala.

[ONE POSSIBLE SOLUTION]

class BowlingGame {

  var frames = List[Frame]()

  def roll(pins: Int) {
    if (frames.size > 0 && !frames.last.frameFinished) {
      frames.last.roll2 = pins
      frames.last.frameFinished = true
    } else {
      frames = frames :+ Frame(pins)
    }
  }

  def score: Int = {
    def sumScore(framesLeft: List[Frame], total: Int): Int = {
      var tempTotal = framesLeft.head.sum
      if (framesLeft.tail != Nil && framesLeft.head.sum == 10) {
        tempTotal += framesLeft(1).roll1
        if (framesLeft.head.strike) {
          if (!framesLeft(1).strike) tempTotal += framesLeft(1).roll2
          else tempTotal += framesLeft(2).roll1
        }
      }
      if (!framesLeft.tail.isEmpty && (frames.size - framesLeft.tail.size) < 10) sumScore(framesLeft.tail, total + tempTotal)
      else total + tempTotal
    }
    sumScore(frames, 0)
  }

  case class Frame(roll1: Int, var roll2: Int = 0) {
    def strike = roll1 == 10
    var frameFinished: Boolean = false
    if (strike) frameFinished = true
    def sum = roll1 + roll2
  }

}

The solution code can be found in BowlingGame.scala solution.

What was your solution? Post it as a comment so that we can compare different ways to solve this kata.

One thought on “Scala Tutorial Through Katas: Bowling Game (Medium)

  1. paul kinsky (@voidfraction)

    I’ll solve this later, but one thing to be aware of is that List[T] is a linked list, and therefore doesn’t support random access.

    So these three lines contain three O(n) operation.
    if (frames.size > 0 && !frames.last.frameFinished) {
    frames.last.roll2 = pins
    frames.last.frameFinished = true

    if you instead use lastOption you can avoid the if statement entirely, and reproduce this logic with a single traversal of the list.

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s