Skip to content

Instantly share code, notes, and snippets.

@mark
Last active December 19, 2015 23:29
Show Gist options
  • Save mark/6034787 to your computer and use it in GitHub Desktop.
Save mark/6034787 to your computer and use it in GitHub Desktop.
A shameless Ruby ripoff of @zobar's Scala Bowling kata solution
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
# 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
# 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