-
-
Save Flambino/6d1e85a7cdfcec08f2c4 to your computer and use it in GitHub Desktop.
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
# 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