Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@Flambino
Last active August 29, 2015 14:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Flambino/6d1e85a7cdfcec08f2c4 to your computer and use it in GitHub Desktop.
Save Flambino/6d1e85a7cdfcec08f2c4 to your computer and use it in GitHub Desktop.
# This class models a "frame" in 10 pin bowling.
class Frame
# Number of pins lined up
PIN_COUNT = 10
# The next frame relative to the receiver
attr_reader :next_frame
# Create an array of +count+ linked frames, in which frame N is linked
# to frame N+1 from first to last.
def self.create_list(count)
raise RangeError.new("count must be greater than 0") if count.to_i < 1
frames = [self.new]
(count.to_i - 1).times.inject(frames) do |frames|
frames << frames.last.append_frame
end
end
def initialize #:nodoc:
@shots = []
end
# Returns a dup of the shots in this frame.
def shots
@shots.dup
end
# Append a new frame to the receiver (if one exists, it's returned).
# If the receiver has already recorded 1 or more shots, no new frame
# will be created, and the method will just return the existing
# +next_frame+ which may be +nil+.
def append_frame
return @next_frame if last? && !shots.empty?
@next_frame ||= self.class.new
end
# Tru if the receiver has been played to completion (i.e. no more shots
# are possible).
def played?
shots.count >= shot_limit
end
# Play this frame till it's done. Returns the shots that were recorded
#
# See also #play_one_shot
def play(&block)
shots_played = []
shots_played << play_one_shot(&block) until played?
shots_played
end
# Bowl a single ball. If the frame's been played, +nil+ will be
# returned.
#
# The block will receive the number of pins still standing, and
# the numbers of the shots already taken, and must return the number
# of pins to topple. The number is clamped to what's possible.
def play_one_shot(&block)
return if played?
shot = yield remaining_pins, shots.count
@shots << [0, shot.to_i, remaining_pins].sort[1]
shots.last
end
# True if the receiver has no following frames (see also #next_frame).
def last?
next_frame.nil?
end
# The score for this frame (may not be final).
def score
scored_shots = successive_shots[0, shots_required_for_score]
scored_shots.inject(0, &:+)
end
# True if have all the necessary shots been taken (in this frame or others).
def score_finalized?
successive_shots.count >= shots_required_for_score
end
# The number of pins knocked down in the shots that have been taken.
def pins
shots.inject(0, &:+)
end
# True if the first shot was a strike.
def strike?
shots.first == PIN_COUNT
end
# True if the first 2 shots constitute a spare.
def spare?
!strike? && shots[0, 2].inject(0, &:+) == PIN_COUNT
end
# Shots required in order to calculate the final score for this frame.
# This includes shots already taken this frame.
def shots_required_for_score
strike? || spare? ? 3 : 2
end
# Returns the number of pins currently standing.
def remaining_pins
remaining = PIN_COUNT - (pins % PIN_COUNT)
remaining %= PIN_COUNT if played?
remaining
end
# Collect shots from this frame, and successive frames.
def successive_shots
return [] if @shots.empty?
collected = @shots.dup
collected += next_frame.successive_shots if next_frame
collected
end
private
# The number of shots that can be taken in this frame.
def shot_limit
return 3 if fill_ball?
return 1 if strike?
return 2
end
# Does this frame have a 3rd shot?
def fill_ball?
last? && (strike? || spare?)
end
end
# Tests
require "rspec/autorun"
RSpec.describe Frame do
# Creates a frame with a pre-set @shots array
def create_frame(*shots) #:nodoc:
new_frame = frame
new_frame.instance_variable_set(:@shots, shots)
new_frame
end
# Create several frames with pre-set @shots array
def create_frames(*shots_per_frame) #:nodoc:
frames = shots_per_frame.map do |shots|
frame = Frame.new
frame.instance_variable_set(:@shots, shots)
frame
end
frames.each_cons(2) { |a, b| a.instance_variable_set(:@next_frame, b) }
frames
end
let(:frame) { Frame.new }
let(:strike_frame) { create_frame(10) }
let(:spare_frame) { create_frame(5, 5) }
let(:open_frame) { create_frame(1, 1) }
let(:incomplete_frame) { create_frame(1) }
describe ".create_frames" do
it "returns an array of the given length" do
frames = Frame.create_list(5)
expect(frames).to be_an(Array)
expect(frames.count).to eq 5
end
it "creates a singly-linked list of frames" do
frames = Frame.create_list(2)
expect(frames.first.next_frame).to eq frames.last
end
end
describe "#pins" do
it "returns the sum of the shots" do
expect(Frame.new.pins).to eq 0
expect(create_frame(5, 3).pins).to eq 8
end
end
describe "#strike?" do
it "is true if the first shot knocked down all pins" do
expect(strike_frame.strike?).to be true
end
it "is false for unplayed, open, and spare frames" do
expect(Frame.new.strike?).to be false
expect(open_frame.strike?).to be false
expect(spare_frame.strike?).to be false
end
end
describe "#spare?" do
it "is true if the first two shots knocked down all pins" do
expect(spare_frame.spare?).to be true
end
it "is false for unplayed, open, and strike frames" do
expect(Frame.new.spare?).to be false
expect(open_frame.spare?).to be false
expect(strike_frame.spare?).to be false
end
end
describe "#shots_required_for_score" do
it "is 3 for strikes and spares" do
expect(strike_frame.shots_required_for_score).to eq 3
expect(spare_frame.shots_required_for_score).to eq 3
end
it "is 2 for unplayed, open, and incomplete frames" do
expect(Frame.new.shots_required_for_score).to eq 2
expect(incomplete_frame.shots_required_for_score).to eq 2
end
end
describe "#score_finalized?" do
it "is true for open frames" do
expect(open_frame.score_finalized?).to be true
end
it "is false for unplayed, strike, spare and incomplete frames" do
expect(Frame.new.score_finalized?).to be false
expect(strike_frame.score_finalized?).to be false
expect(spare_frame.score_finalized?).to be false
expect(incomplete_frame.score_finalized?).to be false
end
end
describe "#append_frame" do
it "returns a new frame if none exists" do
frame = Frame.new
appended = frame.append_frame
expect(appended).to be_a(Frame)
end
it "returns the existing next_frame if one exists" do
frame = Frame.new
appended = frame.append_frame
expect(frame.append_frame).to eq appended
end
it "assigns the appended frame to @next_frame" do
frame = Frame.new
appended = frame.append_frame
expect(appended).to eq frame.next_frame
end
it "will only append if the (receiver) frame is blank" do
expect(incomplete_frame.append_frame).to be_nil
end
end
describe "#last?" do
it "is true if there's no next_frame" do
expect(frame.last?).to be true
end
it "is false for is there is a next_frame" do
frame = Frame.new
frame.append_frame
expect(frame.last?).to be false
end
end
describe "#score_finalized?" do
it "is false for incomplete frames" do
expect(frame.score_finalized?).to be false
expect(incomplete_frame.score_finalized?).to be false
end
end
describe "#score" do
it "returns the pins for open and incomplete frames" do
expect(frame.score).to eq 0
expect(open_frame.score).to eq 2
expect(incomplete_frame.score).to eq 1
end
end
context "regular, mid-game frame" do
# Overrides the +let+ above in order to create a frame
# that has another frame appended. This then forms the
# basis for the other existing +let+ blocks
let(:frame) do
new_frame = Frame.new
new_frame.append_frame
new_frame
end
describe "#played?" do
it "is true for strikes, spares, and open frames" do
expect(strike_frame.played?).to be true
expect(spare_frame.played?).to be true
expect(open_frame.played?).to be true
end
it "is false for unplayed and incomplete frames" do
expect(Frame.new.played?).to be false
expect(incomplete_frame.played?).to be false
end
end
describe "#remaining_pins" do
it "returns the number of pins still standing" do
# TODO: Too many tests in this block
expect(create_frame(10).remaining_pins).to eq 0
expect(create_frame(5, 5).remaining_pins).to eq 0
expect(create_frame(1, 1).remaining_pins).to eq 8
expect(create_frame(1).remaining_pins).to eq 9
expect(create_frame.remaining_pins).to eq 10
end
end
describe "#play_one_shot" do
it "records the block's return value as a shot" do
f = frame
f.play_one_shot { 3 }
expect(f.shots.last).to eq 3
end
it "clamps the block's return value if necessary" do
f = frame
f.play_one_shot { -20 }
expect(f.shots.last).to eq 0
f.play_one_shot { 20 }
expect(f.shots.last).to eq 10
end
it "passes the remaining pins to the block" do
incomplete_frame.play do |remaining_pins|
expect(remaining_pins).to eq 9
10 # just return something
end
end
it "passes shot count to the block" do
incomplete_frame.play do |_, shot_count|
expect(shot_count).to eq 1
10 # just return something
end
end
it "returns the shot taken" do
returned = frame.play_one_shot { 3 }
expect(returned).to eq 3
end
it "returns nil if the frame's already played" do
returned = strike_frame.play_one_shot { 3 }
expect(returned).to be_nil
end
end
describe "#play" do
it "calls the block 2 times at most" do
call_count = 0
frame.play { call_count += 1; 1 }
expect(call_count).to eq 2
end
it "calls the block once if there's a strike" do
call_count = 0
frame.play { call_count += 1; 10 }
expect(call_count).to eq 1
end
it "returns the shots taken" do
shots = frame.play { 5 }
expect(shots).to eq [5, 5]
shots = incomplete_frame.play { 5 }
expect(shots).to eq [5]
end
end
describe "#score_finalized?" do
it "is true if the frame's open" do
expect(open_frame.score_finalized?).to be true
end
it "is false for strike and spare frames without a enough successive shots" do
expect(strike_frame.score_finalized?).to be false
expect(spare_frame.score_finalized?).to be false
end
end
describe "#score" do
it "returns a score based on multiple frames when necessary" do
spare = create_frames([5, 5], [2, 2])
expect(spare.first.score).to eq 12
strike = create_frames([10], [10], [2, 2])
expect(strike.first.score).to eq 22
end
end
end
context "last frame" do
describe "#played?" do
it "is false for non-strike, non-spare frames with less than 3 shots" do
expect(frame.played?).to be false
expect(strike_frame.played?).to be false
expect(spare_frame.played?).to be false
expect(incomplete_frame.played?).to be false
end
it "is true for non-strike, non-spare frames with 2 shots" do
expect(open_frame.played?).to be true
end
it "is true for strike and spare frames with 3 shots" do
expect(create_frame(10, 1, 0).played?).to be true
expect(create_frame(5, 5, 0).played?).to be true
end
end
describe "#remaining_pins" do
it "returns the number of pins still standing, accounting for pins being re-racked" do
# TODO: Waaay too many tests in this block
expect(create_frame(10).remaining_pins).to eq 10
expect(create_frame(5, 5).remaining_pins).to eq 10
expect(create_frame(1, 1).remaining_pins).to eq 8
expect(create_frame(1).remaining_pins).to eq 9
expect(create_frame.remaining_pins).to eq 10
expect(create_frame(10, 10).remaining_pins).to eq 10
expect(create_frame(10, 10, 5).remaining_pins).to eq 5
expect(create_frame(10, 10, 10).remaining_pins).to eq 0
expect(create_frame(5, 5, 10).remaining_pins).to eq 0
end
end
describe "#play" do
it "calls the block 3 times if necessary" do
call_count = 0
frame.play { call_count += 1; 10 }
expect(call_count).to eq 3
end
end
describe "#score_finalized?" do
it "is true if the frame's open" do
expect(open_frame.score_finalized?).to be true
end
it "is true if the frame has a fill ball and 3 shots have been taken" do
expect(create_frame(5, 5, 5).score_finalized?).to be true
expect(create_frame(10, 10, 1).score_finalized?).to be true
end
it "is false if the frame's a strike or spare with no 3rd shot" do
expect(strike_frame.score_finalized?).to be false
expect(spare_frame.score_finalized?).to be false
end
end
describe "#score" do
it "returns sum of the pins toppled" do
expect(create_frame(10, 5, 5).score).to eq 20
expect(create_frame(10, 10, 10).score).to eq 30
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment