Last active
December 19, 2015 23:29
-
-
Save mark/6034787 to your computer and use it in GitHub Desktop.
A shameless Ruby ripoff of @zobar's Scala Bowling kata solution
This file contains hidden or 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
require_relative 'streamer' | |
class Bowling | |
attr_reader :score | |
class GameTooShort < StandardError; end | |
class GameTooLong < StandardError; end | |
class SpareTooEarly < StandardError; end | |
class StrikeTooLate < StandardError; end | |
class TooManyPins < StandardError; end | |
def initialize(throws) | |
@score = game_score(1, Streamer.new(throws)) | |
end | |
def game_score(frame, throws) | |
if frame == 10 | |
throws.match('X', 'X', 'X') { return 30 } | |
throws.match('X', 'X', :digit) { |d| return 20 + d } | |
throws.match('X', :ignore, '/') { return 20 } | |
throws.match('X', :any, :any) { |a, b| return 10 + open_score(a, b) } | |
throws.match('X', :ignore) { raise GameTooShort } | |
throws.match(:ignore, '/', :rest) { |rest| return spare_score(rest) } | |
throws.match(:any, :any) { |a, b| return open_score(a, b) } | |
end | |
throws.match('X', :rest) { |rest| return strike_score(rest) + game_score(frame+1, rest) } | |
throws.match(:ignore, '/', :rest) { |rest| return spare_score(rest) + game_score(frame+1, rest) } | |
throws.match(:any, :any, :rest) { |a, b, rest| return open_score(a, b) + game_score(frame+1, rest) } | |
throws.match(:ignore) { raise GameTooLong } | |
end | |
def open_score(a, b) | |
raise StrikeTooLate if b == 'X' | |
result = throw_score(a) + throw_score(b) | |
raise TooManyPins if result >= 10 | |
result | |
end | |
def spare_score(rest) | |
10 + throw_score(rest.head) | |
end | |
def strike_score(rest) | |
rest.match(:ignore, '/', :rest) { |r| return 10 + 10 } | |
rest.match(:any, :any, :rest) { |a, b, r| return 10 + throw_score(a) + throw_score(b) } | |
rest.match(:ignore) { raise GameTooShort } | |
end | |
def throw_score(t) | |
case t | |
when '-' then 0 | |
when 'X' then 10 | |
when /\d/ then t.to_i | |
else raise SpareTooEarly | |
end | |
end | |
end |
This file contains hidden or 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
# DO NOT LOOK AT THE MAN BEHIND THE MIRROR... | |
class Streamer | |
def initialize(string) | |
@stream = string.split '' | |
end | |
def head | |
@stream[0] | |
end | |
def length | |
@stream.length | |
end | |
def match(*pattern) | |
return false unless length_matches(pattern) | |
pattern.each_with_index do |element, idx| | |
unless match_char(element, @stream[idx]) | |
return false | |
end | |
end | |
yield(*pull_matches(pattern)) if block_given? | |
true | |
end | |
private | |
def length_matches(pattern) | |
if pattern == [:empty] | |
length == 0 | |
elsif pattern[-1] == :rest | |
length >= pattern.length - 1 | |
else | |
length == pattern.length | |
end | |
end | |
def match_char(element, char) | |
case element | |
when String | |
element == char | |
when :digit | |
char =~ /\d/ | |
when :any, :ignore, :rest | |
true | |
when :empty | |
char.nil? | |
end | |
end | |
def pull_matches(pattern) | |
[].tap do |matches| | |
pattern.each_with_index do |element, idx| | |
char = @stream[idx] | |
case element | |
when :digit | |
matches << char.to_i | |
when :any | |
matches << char | |
when :rest | |
matches << rest(idx) | |
when String, :ignore, :empty | |
# Do nothing | |
end | |
end | |
end | |
end | |
def rest(start_index) | |
Streamer.new(@stream[start_index..-1].join) | |
end | |
end |
This file contains hidden or 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
# It might not be pretty, but at least it's tested... | |
gem 'minitest' | |
require 'minitest/autorun' | |
require_relative 'streamer' | |
describe Streamer do | |
describe :length do | |
subject { Streamer.new("foobarbaz") } | |
it "should split characters" do | |
subject.length.must_equal 9 | |
end | |
end | |
describe "with characters" do | |
subject { Streamer.new('f') } | |
it "should match characters" do | |
subject.match('f').must_equal true | |
end | |
it "should not match different characters" do | |
subject.match('z').must_equal false | |
end | |
it "should not match as a digit" do | |
subject.match(:digit).must_equal false | |
end | |
it "should match as any" do | |
subject.match(:any).must_equal true | |
end | |
it "should yield the character" do | |
subject.match(:any) { |c| c.must_equal 'f' } | |
end | |
end | |
describe "with digits" do | |
subject { Streamer.new('9') } | |
it "should match as a character" do | |
subject.match('9').must_equal true | |
end | |
it "should match as a digit" do | |
subject.match(:digit).must_equal true | |
end | |
it "should match as :any" do | |
subject.match(:any).must_equal true | |
end | |
it "should yield the digit as a digit" do | |
subject.match(:digit) { |i| i.must_equal 9 } | |
end | |
it "should not yield anything as :ignore" do | |
subject.match(:ignore) { |*args| args.length.must_equal 0 } | |
end | |
end | |
describe "matching any" do | |
it "should match a character" do | |
Streamer.new('a').match(:any).must_equal true | |
end | |
it "shouldn't match a lack of a character" do | |
Streamer.new('').match(:any).must_equal false | |
end | |
end | |
describe "with multiple characters" do | |
subject { Streamer.new('abc') } | |
it "should advance the index" do | |
subject.match('a', 'b', 'c').must_equal true | |
subject.match('a').must_equal false | |
end | |
end | |
describe "with multicharacter matches" do | |
subject { Streamer.new('abc') } | |
it "should match sequences" do | |
subject.match('a', 'b', 'c').must_equal true | |
end | |
it "should yield all of the elements" do | |
subject.match(:any, :any, :any) do |*args| | |
args.length.must_equal 3 | |
args[0].must_equal 'a' | |
args[1].must_equal 'b' | |
args[2].must_equal 'c' | |
end | |
end | |
end | |
describe "with rest" do | |
subject { Streamer.new('ab') } | |
it "should match :rest" do | |
subject.match(:rest).must_equal true | |
subject.match('a', :rest).must_equal true | |
subject.match('a', 'b', :rest).must_equal true | |
end | |
it "should pass a stream into the block" do | |
subject.match(:rest) do |*args| | |
args.length.must_equal 1 | |
args[0].must_be_instance_of Streamer | |
args[0].match('a', 'b').must_equal true | |
end | |
subject.match('a', :rest) do |*args| | |
args.length.must_equal 1 | |
args[0].must_be_instance_of Streamer | |
args[0].match('b').must_equal true | |
end | |
subject.match('a', 'b', :rest) do |*args| | |
args.length.must_equal 1 | |
args[0].must_be_instance_of Streamer | |
args[0].match(:ignore).must_equal false | |
end | |
end | |
end | |
describe "with empty" do | |
let(:empty) { Streamer.new('') } | |
let(:not_empty) { Streamer.new('a') } | |
it "should match empty only when it's empty" do | |
empty.match(:any).must_equal false | |
empty.match(:empty).must_equal true | |
empty.match().must_equal true | |
end | |
it "should not match empty when it's not empty" do | |
not_empty.match(:any).must_equal true | |
not_empty.match(:empty).must_equal false | |
not_empty.match().must_equal false | |
end | |
end | |
describe "working together" do | |
def sum(accum, stream) | |
stream.match(:digit, :rest) { |d, r| return sum(d + accum, r) } | |
stream.match(:empty) { return accum } | |
end | |
it "should add the digits" do | |
sum(0, Streamer.new('123')).must_equal 6 | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment