Skip to content

Instantly share code, notes, and snippets.

@jefflunt
Created November 21, 2013 19:58
Show Gist options
  • Save jefflunt/7588501 to your computer and use it in GitHub Desktop.
Save jefflunt/7588501 to your computer and use it in GitHub Desktop.
Bowling game scorer with visual score output and per-frame or aggregate scoring.
# Contains a list of rolls that a bowling player throws, and from that can tell
# you the score of the game in progress or finished, the visual score (with 'X'
# and '/', etc.), and the score of any frame within the game.
#
# Examples:
#
# > g = Game.new
# => []
#
# > g.roll(3)
# => [3]
#
# > g.roll(7)
# => [3, 7]
#
# > g.score
# => 10
# > g.visual_score
# => [['3', '/']]
#
# > g.roll(10)
# => [3, 7, 10]
# > g.frames
# => [[3, 7], [10]]
# > g.score
# => 30
# > g.visual_score
# => [['3', '/'], ['X']]
#
# Also handles the special case of the last frame being allowed to contain upto
# 3 ball scores, and scores them appropriately.
class Game
def initialize
@ball_scores = []
end
# Record a score for a single ball thrown.
#
# Returns the new list of ball scores, or nil if the scores were unmodified
def roll(ball_score)
@ball_scores.push(ball_score)
end
# Current total score for the game
def score
score_frame(self.frames.size - 1) # unless self.finished?
end
def visual_frame(frame_index)
frame = self.frames[frame_index]
case self.frame_state(frame)
when :incomplete then frame.collect{|score| score.to_s }
when :open_frame then frame.collect{|score| score.to_s }
when :spare then [frame[0].to_s, '/']
when :strike then ['X']
when :last_frame then visual_last_frame(frame)
end
end
def visual_last_frame(frame)
last_frame = []
case frame[0]
when 10 then last_frame.push('X')
else last_frame.push(frame[0].to_s)
end
if frame[0] + frame[1] == 10
last_frame.push('/')
elsif frame[0] == 10 && frame[1] == 10
last_frame.push('X')
else
last_frame.push(frame[1].to_s)
end
case frame[2]
when 10 then last_frame.push('X')
else last_frame.push(frame[2].to_s)
end
end
def visual_score
visual_score = []
self.frames.each_with_index do |frame, index|
visual_score.push(visual_frame(index))
end
visual_score
end
# A game is finished when it contains 10 frames, and the last one is NOT
# :incomplete
def finished?
last_frame = self.frames[9]
if frame_state(last_frame) == :open_frame
return true
elsif frame_state(last_frame) == :last_frame
return true
else
return false
end
end
# Given a frame index, the score for the entire game UPTO (and including) the
# specified frame index
def score_frame(frame_index)
score = 0
frames = self.frames
for i in 0..frame_index do
score += frames[i].reduce(:+) + bonus_points(i)
end
score
end
# Takes all the ball scores thrown in the game so far, and split them into
# frames.
#
# frames([6, 1, 10, 7, 3, 5])
# => [[6, 1], [10], [7, 3], [5]]
#
# Where each sub-array contains the balls thrown in each frame. Where there
# are strikes, the frame will only contain a single ball. Where there are
# spares or open frames the frame will contain two balls. For a frame that's
# Not yet complete (the last frame in the above example) it will only contain
# the first ball thrown, since that's the only ball that applies to that frame
# so far.
def frames
frames = []
scores_copy = Array.new(@ball_scores)
while scores_copy.size > 0 do
new_frame = []
new_frame.push(scores_copy.shift)
if new_frame.reduce(:+) == 10
frames.push(new_frame)
else
new_frame.push(scores_copy.shift) unless scores_copy.size == 0
frames.push(new_frame)
end
end
# Handle the special case that the last frame contains as many balls as
# necessary
if frames.size > 10
collapsed_last_frame = frames[9]
(frames.size - 10).times do
collapsed_last_frame.push(frames.delete_at(10))
end
frames[9] = collapsed_last_frame.flatten
end
frames
end
# Returns a symbol indicating the state of a given frame index. One of:
#
# :incomplete (not enough balls in the frame to complete the frame)
# :open_frame (two balls in the frame whose score is < 10)
# :spare (two balls in the frame whose score is exactly 10)
# :strike (one ball in the frame whose score is exactly 10)
def frame_state(frame_scores)
if frame_scores.size == 1
return :incomplete if frame_scores.reduce(:+) < 10
return :strike if frame_scores.reduce(:+) == 10
elsif frame_scores.size == 2
return :open_frame if frame_scores.reduce(:+) < 10
return :spare if frame_scores.reduce(:+) == 10
elsif frame_scores.size == 3
return :last_frame
end
end
# Given a frame index, returns the number of bonus points from future frames
# (if any) that also count toward that frame.
#
# :incomplete => bonus_points is 0
# :open_frame => bonus_points is 0
# :spare => bonus_points is next 1 ball score
# :strike => bonus_points is sum of next 2 ball scores
def bonus_points(frame_index)
bonus_points = case self.frame_state(self.frames[frame_index])
when :incomplete then 0
when :last_frame then 0
when :open_frame then 0
when :spare then balls_following(frame_index, 1).reduce(:+).to_i
when :strike then balls_following(frame_index, 2).reduce(:+).to_i
end
bonus_points
end
# Given a frame index, returns an array that contains AT MOST 'num_balls' that
# follow after the specified frame index. This is used primarily as a
# supporting method to 'bonus_points', so that it's simple to get the balls
# that follow a given frame, and therefore the total bonus points on a frame.
def balls_following(frame_index, num_balls)
balls_following = []
frames = self.frames
for i in 0..frame_index
frames.shift
end
remaining_balls = frames.flatten
num_balls.times do
balls_following.push(remaining_balls.shift) unless remaining_balls.size == 0
end
balls_following
end
end
gem 'minitest'
require 'minitest/autorun'
require_relative '../game'
class TestGame < Minitest::Test
def setup
@game = Game.new
end
def test_roll
assert_equal [3], @game.roll(3)
end
def test_game_finished?
@game.roll(10) # First 5 frames
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10) # Second 5 frames
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10)
refute @game.finished?
@game.roll(10) # First bonus ball
refute @game.finished?
@game.roll(10) # Second bonus ball
assert @game.finished?
end
def test_empty_game
assert_equal 0, @game.score
end
def test_single_ball_game
@game.roll(6)
assert_equal 6, @game.score
end
def test_multiball_game
@game.roll(6)
@game.roll(1)
assert_equal 7, @game.score
@game.roll(5)
assert_equal 12, @game.score
end
def test_frames_complex_game
@game.roll(6)
@game.roll(1)
@game.roll(10)
@game.roll(7)
@game.roll(3)
@game.roll(5)
assert_equal [[6, 1], [10], [7, 3], [5]], @game.frames
end
def test_frames_end_on_spare
@game.roll(10) # First 5 frames
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10) # Second 5 frames
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(7)
@game.roll(3)
@game.roll(10) # Two bonus balls
assert_equal [[10], [10], [10], [10], [10], [10], [10], [10], [10], [7, 3, 10]], @game.frames
end
def test_frames_all_strikes_game
@game.roll(10) # First 5 frames
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10) # Second 5 frames
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10) # Two bonus balls
@game.roll(10)
assert_equal [[10], [10], [10], [10], [10], [10], [10], [10], [10], [10, 10, 10]], @game.frames
end
def test_frame_state
@game.roll(6)
@game.roll(1)
@game.roll(10)
@game.roll(7)
@game.roll(3)
@game.roll(5)
assert_equal :open_frame, @game.frame_state(@game.frames[0])
assert_equal :strike, @game.frame_state(@game.frames[1])
assert_equal :spare, @game.frame_state(@game.frames[2])
assert_equal :incomplete, @game.frame_state(@game.frames[3])
end
def test_balls_following
@game.roll(6)
@game.roll(1)
@game.roll(10)
@game.roll(7)
@game.roll(3)
assert_equal [10], @game.balls_following(0, 1)
assert_equal [10, 7], @game.balls_following(0, 2)
assert_equal [7, 3], @game.balls_following(1, 2)
assert_equal [], @game.balls_following(999, 1)
end
def test_bonus_points
@game.roll(6)
@game.roll(1)
@game.roll(10)
@game.roll(7)
@game.roll(3)
@game.roll(5)
assert_equal 0, @game.bonus_points(0)
assert_equal 10, @game.bonus_points(1)
assert_equal 5, @game.bonus_points(2)
assert_equal 0, @game.bonus_points(3)
end
def test_score_frame
@game.roll(6)
@game.roll(1)
@game.roll(10)
@game.roll(7)
@game.roll(3)
@game.roll(5)
assert_equal 7, @game.score_frame(0)
assert_equal 27, @game.score_frame(1)
assert_equal 42, @game.score_frame(2)
assert_equal 47, @game.score_frame(3)
end
def test_score_game
@game.roll(6)
@game.roll(1)
@game.roll(10)
@game.roll(7)
@game.roll(3)
@game.roll(5)
assert_equal 47, @game.score
end
def test_score_turkey
@game.roll(10)
@game.roll(10)
@game.roll(10)
assert_equal 30, @game.score_frame(0)
assert_equal 50, @game.score_frame(1)
assert_equal 60, @game.score_frame(2)
assert_equal 60, @game.score
end
def test_score_perfect_game
# A perfect game being 10 frames with strikes, followed by two bonus balls
# that are also strikes
@game.roll(10) # First 5 frames
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10) # Second 5 frames
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(10) # Bonus balls at the end of the game
@game.roll(10)
assert_equal 300, @game.score
end
def test_visual_frame
@game.roll(3)
@game.roll(5)
@game.roll(10)
@game.roll(2)
@game.roll(8)
assert_equal ['3', '5'], @game.visual_frame(0)
assert_equal ['X'], @game.visual_frame(1)
assert_equal ['2', '/'], @game.visual_frame(2)
end
def test_visual_last_frame
assert_equal ['3', '/', '5'], @game.visual_last_frame([3, 7, 5])
assert_equal ['3', '/', 'X'], @game.visual_last_frame([3, 7, 10])
assert_equal ['X', '2', '5'], @game.visual_last_frame([10, 2, 5])
assert_equal ['X', '5', '5'], @game.visual_last_frame([10, 5, 5])
assert_equal ['X', 'X', '3'], @game.visual_last_frame([10, 10, 3])
assert_equal ['X', 'X', 'X'], @game.visual_last_frame([10, 10, 10])
end
def test_visual_score_for_in_progress_game
@game.roll(3)
@game.roll(5)
@game.roll(10)
@game.roll(2)
@game.roll(8)
@game.roll(9)
assert_equal [['3', '5'], ['X'], ['2', '/'], ['9']], @game.visual_score
end
def test_visual_score_for_game_ending_in_spare
@game.roll(3)
@game.roll(5)
@game.roll(10)
@game.roll(2)
@game.roll(8)
@game.roll(9)
@game.roll(1)
@game.roll(10)
@game.roll(3)
@game.roll(5)
@game.roll(2)
@game.roll(0)
@game.roll(10)
@game.roll(10)
@game.roll(10)
@game.roll(8)
@game.roll(2)
assert_equal [['3', '5'],
['X'],
['2', '/'],
['9', '/'],
['X'],
['3', '5'],
['2', '0'],
['X'],
['X'],
['X','8', '2']], @game.visual_score
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment