diff --git a/exercises/bowling/.version b/exercises/bowling/.version new file mode 100644 index 0000000000..d8263ee986 --- /dev/null +++ b/exercises/bowling/.version @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/exercises/bowling/bowling_test.rb b/exercises/bowling/bowling_test.rb index 898233cee8..135610ffa2 100755 --- a/exercises/bowling/bowling_test.rb +++ b/exercises/bowling/bowling_test.rb @@ -3,224 +3,200 @@ require 'minitest/autorun' require_relative 'bowling' -class GameTest < Minitest::Test +# Test data version: +# 0a51cfc +class BowlingTest < Minitest::Test def setup @game = Game.new end - def test_must_be_able_to_roll_with_number_of_pins - assert_respond_to @game, :roll - assert_equal 1, @game.method(:roll).arity + def roll(rolls) + rolls.each { |pins| @game.roll(pins) } end - def test_must_have_a_score - skip - assert_respond_to @game, :score + def test_should_be_able_to_score_a_game_with_all_zeros + # skip + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert_equal 0, @game.score end - def test_should_be_able_to_score_open_frame + def test_should_be_able_to_score_a_game_with_no_strikes_or_spares skip - @game.roll(3) - @game.roll(4) - roll_n_times(18, 0) - assert_equal 7, @game.score + roll([3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6]) + assert_equal 90, @game.score end - def test_should_be_able_to_score_multiple_frames + def test_a_spare_followed_by_zeros_is_worth_ten_points skip - [3, 4, 2, 3, 5, 2].each do |pins| - @game.roll pins - end - roll_n_times(14, 0) - assert_equal 19, @game.score + roll([6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert_equal 10, @game.score end - def test_should_score_a_game_with_all_gutterballs + def test_points_scored_in_the_roll_after_a_spare_are_counted_twice skip - roll_n_times(20, 0) - assert_equal 0, @game.score + roll([6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert_equal 16, @game.score end - def test_should_score_a_game_with_all_single_pin_rolls + def test_consecutive_spares_each_get_a_one_roll_bonus skip - roll_n_times(20, 1) - assert_equal 20, @game.score + roll([5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert_equal 31, @game.score end - def test_should_allow_game_with_all_open_frames + def test_a_spare_in_the_last_frame_gets_a_one_roll_bonus_that_is_counted_once skip - roll_n_times(10, [3, 6]) - assert_equal 90, @game.score + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7]) + assert_equal 17, @game.score end - def test_should_correctly_score_a_strike_that_is_not_on_the_last_frame + def test_a_strike_earns_ten_points_in_frame_with_a_single_roll skip - @game.roll(10) - @game.roll(5) - @game.roll(3) - roll_n_times(16, 0) - - assert_equal 26, @game.score + roll([10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert_equal 10, @game.score end - def test_should_score_a_spare_that_is_not_on_the_last_frame + def test_points_scored_in_the_two_rolls_after_a_strike_are_counted_twice_as_a_bonus skip - @game.roll(5) - @game.roll(5) - @game.roll(3) - @game.roll(4) - roll_n_times(16, 0) - - assert_equal 20, @game.score + roll([10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert_equal 26, @game.score end - def test_should_score_multiple_strikes_in_a_row + def test_consecutive_strikes_each_get_the_two_roll_bonus skip - @game.roll(10) - @game.roll(10) - @game.roll(10) - @game.roll(5) - @game.roll(3) - roll_n_times(12, 0) - + roll([10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) assert_equal 81, @game.score end - def test_should_score_multiple_spares_in_a_row + def test_a_strike_in_the_last_frame_gets_a_two_roll_bonus_that_is_counted_once skip - @game.roll(5) - @game.roll(5) - @game.roll(3) - @game.roll(7) - @game.roll(4) - @game.roll(1) - roll_n_times(14, 0) - - assert_equal 32, @game.score + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 1]) + assert_equal 18, @game.score end - def test_should_allow_fill_balls_when_the_final_frame_is_strike + def test_rolling_a_spare_with_the_two_roll_bonus_does_not_get_a_bonus_roll skip - roll_n_times(18, 0) - @game.roll(10) - @game.roll(7) - @game.roll(1) - - assert_equal 18, @game.score + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 3]) + assert_equal 20, @game.score end - def test_should_allow_fill_ball_in_last_frame_if_spare + def test_strikes_with_the_two_roll_bonus_do_not_get_bonus_rolls skip - roll_n_times(18, 0) - @game.roll(9) - @game.roll(1) - @game.roll(7) - - assert_equal 17, @game.score + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10]) + assert_equal 30, @game.score end - def test_should_allow_fill_balls_to_be_strike + def test_a_strike_with_the_one_roll_bonus_after_a_spare_in_the_last_frame_does_not_get_a_bonus skip - roll_n_times(18, 0) - @game.roll(10) - @game.roll(10) - @game.roll(10) - - assert_equal 30, @game.score + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 10]) + assert_equal 20, @game.score end - def test_should_score_a_perfect_game + def test_all_strikes_is_a_perfect_game skip - roll_n_times(12, 10) + roll([10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]) assert_equal 300, @game.score end - def test_should_not_allow_rolls_with_negative_pins + def test_rolls_can_not_score_negative_points skip - assert_raises( - RuntimeError, - 'Pins must have a value from 0 to 10') do - @game.roll(-1) - end + assert_raises StandardError do + roll([-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + @game.score + end end - def test_should_not_allow_rolls_better_than_strike + def test_a_roll_can_not_score_more_than_10_points skip - assert_raises( - RuntimeError, - 'Pins must have a value from 0 to 10') do - @game.roll(11) - end + assert_raises StandardError do + roll([11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + @game.score + end end - def test_should_not_allow_two_normal_rolls_better_than_strike + def test_two_rolls_in_a_frame_can_not_score_more_than_10_points skip - assert_raises RuntimeError, 'Pin count exceeds pins on the lane' do - @game.roll(5) - @game.roll(6) + assert_raises StandardError do + roll([5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + @game.score end end - def test_should_not_allow_two_normal_rolls_better_than_strike_in_last_frame + def test_two_bonus_rolls_after_a_strike_in_the_last_frame_can_not_score_more_than_10_points skip - roll_n_times(18, 0) - assert_raises RuntimeError, 'Pin count exceeds pins on the lane' do - @game.roll(10) - @game.roll(5) - @game.roll(6) + assert_raises StandardError do + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 5, 6]) + @game.score end end - def test_should_not_allow_to_take_score_at_the_beginning + def test_an_unstarted_game_can_not_be_scored skip - assert_raises( - RuntimeError, - 'Score cannot be taken until the end of the game', - ) do + assert_raises StandardError do + roll([]) @game.score end end - def test_should_not_allow_to_take_score_before_game_has_ended + def test_an_incomplete_game_can_not_be_scored skip - roll_n_times(19, 5) - assert_raises( - RuntimeError, - 'Score cannot be taken until the end of the game') do - @game.score - end + assert_raises StandardError do + roll([0, 0]) + @game.score + end end - def test_should_not_allow_rolls_after_the_tenth_frame + def test_a_game_with_more_than_ten_frames_can_not_be_scored skip - roll_n_times(20, 0) - assert_raises( - RuntimeError, - 'Should not be able to roll after game is over', - ) do - @game.roll(0) + assert_raises StandardError do + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + @game.score end end - def test_should_not_calculate_score_before_fill_balls_have_been_played + def test_bonus_rolls_for_a_strike_in_the_last_frame_must_be_rolled_before_score_can_be_calculated skip - roll_n_times(10, 10) + assert_raises StandardError do + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10]) + @game.score + end + end - assert_raises RuntimeError, 'Game is not yet over, cannot score!' do + def test_both_bonus_rolls_for_a_strike_in_the_last_frame_must_be_rolled_before_score_can_be_calculated + skip + assert_raises StandardError do + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10]) @game.score end end - def roll_n_times(rolls, pins) - rolls.times do - Array(pins).each { |value| @game.roll(value) } + def test_bonus_roll_for_a_spare_in_the_last_frame_must_be_rolled_before_score_can_be_calculated + skip + assert_raises StandardError do + roll([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3]) + @game.score end end - private :roll_n_times - # Don't forget to define a constant VERSION inside of BookKeeping. + # Problems in exercism evolve over time, as we find better ways to ask + # questions. + # The version number refers to the version of the problem you solved, + # not your solution. + # + # Define a constant named VERSION inside of the top level BookKeeping + # module, which may be placed near the end of your file. + # + # In your file, it will look like this: + # + # module BookKeeping + # VERSION = 1 # Where the version number matches the one in the test. + # end + # + # If you are curious, read more about constants on RubyDoc: + # http://ruby-doc.org/docs/ruby-doc-bundle/UsersGuide/rg/constants.html + def test_bookkeeping skip - assert_equal 1, BookKeeping::VERSION + assert_equal 2, BookKeeping::VERSION end end diff --git a/exercises/bowling/example.rb b/exercises/bowling/example.rb index 7cbac93d45..29f93240c9 100644 --- a/exercises/bowling/example.rb +++ b/exercises/bowling/example.rb @@ -1,9 +1,9 @@ module BookKeeping - VERSION = 1 + VERSION = 2 end class Game - RULES = { MIN: 0, MAX: 10 }.freeze + PINS = { MIN: 0, MAX: 10 }.freeze at_exit { public :roll, :score } private @@ -20,7 +20,7 @@ def roll(pins) end def validate(pins) - raise 'Invalid number of pins' unless (RULES[:MIN]..RULES[:MAX]).cover?(pins) + raise 'Invalid number of pins' unless (PINS[:MIN]..PINS[:MAX]).cover?(pins) raise 'Too many pins in frame' unless valid_frame?(pins) raise 'Game is over, no rolls allowed' if game_complete? end @@ -28,8 +28,8 @@ def validate(pins) def valid_frame?(pins) last_roll_was_strike = @score_card[current_frame].last == 10 - (last_frame? && last_roll_was_strike) || - @score_card[current_frame].last.to_i + pins <= RULES[:MAX] + (last_frame? && last_roll_was_strike || spare?) || + @score_card[current_frame].last.to_i + pins <= PINS[:MAX] end def score @@ -40,10 +40,10 @@ def score end def score_frame(f, i) - strike_or_spare = [f.first, f.inject(:+)].any? {|e| e == RULES[:MAX]} + strike_or_spare = [f.first, f.inject(:+)].any? { |e| e == PINS[:MAX] } if strike_or_spare - special(@score_card[i], @score_card[i+1], @score_card[i+2]) + special(@score_card[i], @score_card[i + 1], @score_card[i + 2]) else f.reduce(:+) end diff --git a/exercises/bowling/example.tt b/exercises/bowling/example.tt new file mode 100644 index 0000000000..99bdc986c1 --- /dev/null +++ b/exercises/bowling/example.tt @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +gem 'minitest', '>= 5.0.0' +require 'minitest/autorun' +require_relative 'bowling' + +# Test data version: +# <%= sha1 %> +class BowlingTest < Minitest::Test + def setup + @game = Game.new + end + + def roll(rolls) + rolls.each { |pins| @game.roll(pins) } + end +<% test_cases.each do |test_case| %> + def <%= test_case.test_name %> + <%= test_case.skipped %> + <%= test_case.work_load %> + end +<% end %> +<%= IO.read(XRUBY_LIB + '/bookkeeping.md') %> + def test_bookkeeping + skip + assert_equal <%= version.next %>, BookKeeping::VERSION + end +end diff --git a/lib/bowling_cases.rb b/lib/bowling_cases.rb new file mode 100644 index 0000000000..e9a9b40aab --- /dev/null +++ b/lib/bowling_cases.rb @@ -0,0 +1,46 @@ +class BowlingCase < OpenStruct + def test_name + "test_#{description.downcase.tr(' ', '_')}" + end + + def skipped + index.zero? ? '# skip' : 'skip' + end + + def work_load + indent_lines(assert) + end + + private + + def roll + "roll(#{rolls})" + end + + def assert + if assert_error? + [ + 'assert_raises StandardError do', + " #{roll}", + ' @game.score', + 'end' + ] + else + [roll, "assert_equal #{expected}, @game.score"] + end + end + + def assert_error? + expected == -1 + end + + def indent_lines(code) + code.join("\n" + ' ' * 4) + end +end + +BowlingCases = proc do |data| + JSON.parse(data)['score']['cases'].map.with_index do |row, i| + BowlingCase.new(row.merge('index' => i)) + end +end