"Exercise: Bowling"

bowling: rolls
"Score a bowling game.
    Bowling is a game where players roll a heavy ball to knock down pins arranged in a triangle.
    Write code to keep track of the score of a game of bowling.

    Scoring Bowling:
    The game consists of 10 frames. A frame is composed of one or two ball throws with 10 pins standing at frame initialization.
    There are three cases for the tabulation of a frame.
    - An open frame is where a score of less than 10 is recorded for the frame.
    In this case the score for the frame is the number of pins knocked down.
    - A spare is where all ten pins are knocked down by the second throw.
    The total value of a spare is 10 plus the number of pins knocked down in their next throw.
    - A strike is where all ten pins are knocked down by the first throw.
    The total value of a strike is 10 plus the number of pins knocked down in the next two throws.
    If a strike is immediately followed by a second strike, then the value of the first strike cannot be determined until the ball is thrown one more time.

    The tenth frame in the game is a special case.
    If someone throws a spare or a strike then they get one or two fill balls respectively.
    Fill balls exist to calculate the total of the 10th frame.
    Scoring a strike or spare on the fill ball does not give the player more fill balls.
    The total value of the 10th frame is the total number of pins knocked down."
"(self new bowling: #(10 5 5 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)) >>> 48"
"(self new bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 1 9)) >>> 20"
"(self new bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 10 10)) >>> 30"
| pins frames score rollIndex frameIndex first second third errorIncomplete errorTooManyPins ensureAvailable |
	pins := 10.
	frames := 10.
	score := 0.
	rollIndex := 1.
	frameIndex := 1.

	errorIncomplete := [
	                   self error:
		                   'Score cannot be taken until the end of the game' ].
	errorTooManyPins := [
	                    self error: 'Pin count exceeds pins on the lane' ].
	ensureAvailable := [ :needed |
	                   rollIndex + needed - 1 > rolls size ifTrue:
		                   errorIncomplete ].

	(rolls anySatisfy: [ :each | each < 0 ]) ifTrue: [
		self error: 'Negative roll is invalid' ].
	(rolls anySatisfy: [ :each | each > pins ]) ifTrue: [
		self error: 'Pin count exceeds pins on the lane' ].

	[ frameIndex <= frames ] whileTrue: [
			rollIndex > rolls size ifTrue: errorIncomplete.
			first := rolls at: rollIndex.

			frameIndex = frames ifTrue: [
					ensureAvailable value: 2.
					second := rolls at: rollIndex + 1.

					(first = pins or: [ first + second = pins ])
						ifTrue: [
								ensureAvailable value: 3.
								third := rolls at: rollIndex + 2.
								(first = pins and: [
									 second ~= pins and: [ second + third > pins ] ]) ifTrue:
									errorTooManyPins.
								(first ~= pins and: [
									 first + second = pins and: [ third > pins ] ]) ifTrue:
									errorTooManyPins.
								(first = pins and: [ second = pins and: [ third > pins ] ])
									ifTrue: errorTooManyPins.

								score := score + first + second + third.
								rollIndex := rollIndex + 3 ]
						ifFalse: [
								first + second > pins ifTrue: errorTooManyPins.
								score := score + first + second.
								rollIndex := rollIndex + 2 ].

					frameIndex := frameIndex + 1.

					rollIndex <= rolls size ifTrue: [
						self error: 'Cannot roll after game is over' ].
					^ score ].

			first = pins
				ifTrue: [
						ensureAvailable value: 3.
						score := score + pins + (rolls at: rollIndex + 1)
						         + (rolls at: rollIndex + 2).
						rollIndex := rollIndex + 1 ]
				ifFalse: [
						ensureAvailable value: 2.
						second := rolls at: rollIndex + 1.
						first + second > pins ifTrue: errorTooManyPins.
						score := score + first + second.
						first + second = pins ifTrue: [
								ensureAvailable value: 3.
								score := score + (rolls at: rollIndex + 2) ].

						rollIndex := rollIndex + 2 ].

			frameIndex := frameIndex + 1 ].

	rollIndex < rolls size ifTrue: [
		self error: 'Cannot roll after game is over' ].

	^ score


"Tests"
testBowling
"Scoring and validation tests for bowling"
"01 Should be able to score a game with all zeros"
self assert: (task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)) equals: 0.

  "02 Should be able to score a game with no strikes or spares"
  self assert: (task bowling: #(3 6 3 6 3 6 3 6 3 6 3 6 3 6 3 6 3 6 3 6)) equals: 90.

  "03 A spare followed by zeros is worth ten points"
  self assert: (task bowling: #(6 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)) equals: 10.

  "04 Points scored in the roll after a spare are counted twice"
  self assert: (task bowling: #(6 4 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)) equals: 16.

  "05 Consecutive spares each get a one-roll bonus"
  self assert: (task bowling: #(5 5 3 7 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)) equals: 31.

  "06 A spare in the last frame gets a one-roll bonus that is counted once"
  self assert: (task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 3 7)) equals: 17.

  "07 A strike earns ten points in a frame with a single roll"
  self assert: (task bowling: #(10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)) equals: 10.

  "08 Points scored in the two rolls after a strike are counted twice as a bonus"
  self assert: (task bowling: #(10 5 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)) equals: 26.

  "09 Consecutive strikes each get the two-roll bonus"
  self assert: (task bowling: #(10 10 10 5 3 0 0 0 0 0 0 0 0 0 0 0 0)) equals: 81.

  "10 A strike in the last frame gets a two-roll bonus that is counted once"
  self assert: (task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 7 1)) equals: 18.

  "11 Rolling a spare with the two-roll bonus does not get a bonus roll"
  self assert: (task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 7 3)) equals: 20.

  "12 Strikes with the two-roll bonus do not get bonus rolls"
  self assert: (task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 10 10)) equals: 30.

  "13 A strike with the one-roll bonus after a spare in the last frame does not get a bonus"
  self assert: (task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 3 10)) equals: 20.

  "14 All strikes is a perfect game"
  self assert: (task bowling: #(10 10 10 10 10 10 10 10 10 10 10 10)) equals: 300.

  "15 Rolls cannot score negative points"
  self
    should: [ task bowling: #(-1) ]
    raise: Error.

  "16 A roll cannot score more than 10 points"
  self
    should: [ task bowling: #(11) ]
    raise: Error.

  "17 Two rolls in a frame cannot score more than 10 points"
  self
    should: [ task bowling: #(5 6) ]
    raise: Error.

  "18 Bonus roll after a strike in the last frame cannot score more than 10 points"
  self
    should: [ task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 11) ]
    raise: Error.

  "19 Two bonus rolls after a strike in the last frame cannot score more than 10 points"
  self
    should: [ task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 5 6) ]
    raise: Error.

  "20 Two bonus rolls after a strike in the last frame can score more than 10 points if one is a strike"
  self assert: (task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 10 6)) equals: 26.

  "21 The second bonus roll after a strike in the last frame cannot be a strike if the first one is not a strike"
  self
    should: [ task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 6 10) ]
    raise: Error.

  "22 Second bonus roll after a strike in the last frame cannot score more than 10 points"
  self
    should: [ task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 10 11) ]
    raise: Error.

  "23 An unstarted game cannot be scored"
  self
    should: [ task bowling: #() ]
    raise: Error.

  "24 An incomplete game cannot be scored"
  self
    should: [ task bowling: #(0 0) ]
    raise: Error.

  "25 Cannot roll if game already has ten frames"
  self
    should: [ task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0) ]
    raise: Error.

  "26 Bonus roll for a strike in the last frame must be rolled before score can be calculated"
  self
    should: [ task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10) ]
    raise: Error.

  "27 Both bonus rolls for a strike in the last frame must be rolled before score can be calculated"
  self
    should: [ task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 10) ]
    raise: Error.

  "28 Bonus roll for a spare in the last frame must be rolled before score can be calculated"
  self
    should: [ task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 3) ]
    raise: Error.

  "29 Cannot roll after bonus roll for spare"
  self
    should: [ task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 3 2 2) ]
    raise: Error.

  "30 Cannot roll after bonus rolls for strike"
  self
    should: [ task bowling: #(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 3 2 2) ]
    raise: Error