Created
November 21, 2013 19:58
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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