diff --git a/.rspec b/.rspec
new file mode 100644
index 00000000..775c62b0
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,2 @@
+--require spec_helper
+--format documentation
\ No newline at end of file
diff --git a/design.md b/design.md
new file mode 100644
index 00000000..4352e576
--- /dev/null
+++ b/design.md
@@ -0,0 +1,120 @@
+# Bowling Challenge Multi-Class Planned Design Recipe
+
+## 1. Describe the Problem
+
+Count and sum the scores of a bowling game for one player.
+
+### Bowling scoring
+
+Scoring it bowling is pretty tricky. This is a representation of how the logic works.
+
+```mermaid
+flowchart TD
+ F1[Frame 1] --> B1.1{Ball 1}
+ B1.1 -->|Score < 10| B1.1S[Record B1 score]
+ B1.1 -->|Score == 10| NF1.1[Strike: FS pending and
move to next frame]
+ NF1.1 -->|Carry strike from F1| F2
+ B1.2 -->|Frame score == 10| NF1.2[Spare: BS2 pending and move
to next fame]
+ NF1.2 -->|Carry spare from F1| F2
+ B1.1S --> B1.2{Ball 2}
+ B1.2 -->|Frame score < 10| FS1.2[Record F1 score and
move to next frame]
+ FS1.2 --> F2
+
+ F2[Frame 2] --> B2.1{Ball 1}
+ B2.1 -->|Score < 10| B2.1S{B1 score}
+ B2.1 -->|Score == 10| NF2.1{Strike:
FS pending
and move to
next frame}
+ NF2.1 -->|If strike from F1,
carry strikes from F1 & F2| F3
+ NF2.1 -->|If spare from F1,
record F1 bonus| F3
+ NF2.1 -->|If no strike from F1,
carry strike from F2| F3
+ B2.2 -->|Frame score == 10| NF2.2[Spare: BS2 pending and move
to next fame]
+ NF2.2 -->|Carry spare from F2| F3
+ B2.1S -->|Record score.
Next ball.| B2.2{Ball 2}
+ B2.1S -->|Spare from F1| B2.1Ss[Add bonus to
F1 score]
+ B2.1Ss --> B2.2
+ B2.2 -->|Strike from F1| B2.2Ss[Add bonus to
F1 score]
+ B2.2Ss --> F3[Add bonus to
F1 score]
+ B2.2 -->|Frame score < 10| FS2.2[Record frame score and
move to next frame]
+ FS2.2 --> F3[Frame 3]
+```
+
+## 2. Design the Class System
+
+```mermaid
+classDiagram
+ Application <|-- Frame
+ Application <|-- ScoreCard
+ Application <|-- Gameplay
+ Application : +array frames
+ Application : +object gameplay
+ Application : +run()
+ Application : +create_frames()
+ class Frame{
+ +array ball_scores
+ +int bonus_score
+ +bool strike
+ +bool spare
+ +add_ball_score(ball, ball_score)
+ +get_ball_score(ball)
+ +update_bonus_score(score)
+ +bonus_score()
+ +two_balls?()
+ +strike(ball)
+ +strike?()
+ +spare()
+ +spare?()
+ +frame_score()
+ +total_frame_score()
+ }
+ class ScoreCard{
+ +update_pending_bonuses(frames, current_frame)
+ +show_scorecard(frames)
+ -update_pending_strikes(frames, current_frame)
+ -update_pending_spares(frames, current_frame)
+ -scorecard_segment(frame, frame_number)
+ -scorecard_segment_frame_10(frame)
+ }
+ class Gameplay{
+ +int current_frame
+ +int current_ball
+ +object scorecard
+ +score_prompt(frames)
+ +process_current_ball(frame, input)
+ +next_ball(frames)
+ +continue?(frames)
+ +final_score(frames)
+ -validate_input(input, frames)
+ }
+```
+
+
diff --git a/lib/app.rb b/lib/app.rb
new file mode 100644
index 00000000..300ebb48
--- /dev/null
+++ b/lib/app.rb
@@ -0,0 +1,34 @@
+require_relative 'frame'
+require_relative 'gameplay'
+require_relative 'scorecard'
+
+class Application
+ def initialize
+ @frames = {}
+ @gameplay = Gameplay.new
+ create_frames
+ end
+
+ def run
+ loopy = true
+ while loopy do
+ success = @gameplay.score_prompt(@frames)
+ @gameplay.next_ball(@frames) if success == true
+ loopy = @gameplay.continue?(@frames)
+ end
+ @gameplay.final_score(@frames)
+ return
+ end
+
+ def create_frames
+ 10.times do |i|
+ frame = Frame.new
+ @frames[i + 1] = frame
+ end
+ end
+end
+
+unless ENV['ENV'] == 'test'
+ app = Application.new
+ app.run
+end
diff --git a/lib/frame.rb b/lib/frame.rb
new file mode 100644
index 00000000..e5b377ee
--- /dev/null
+++ b/lib/frame.rb
@@ -0,0 +1,58 @@
+class Frame
+ def initialize
+ @ball_scores = [0, 0, 0]
+ @bonus_score = 0
+ @strike = false
+ @spare = false
+ end
+
+ def add_ball_score(ball, ball_score)
+ @ball_scores[ball - 1] = ball_score
+ end
+
+ def get_ball_score(ball)
+ return @ball_scores[ball - 1]
+ end
+
+ def update_bonus_score(score)
+ @bonus_score = score
+ end
+
+ def bonus_score
+ return @bonus_score
+ end
+
+ # Returns true if this is a complete frame with no strike
+ def two_balls?
+ return true if (get_ball_score(1) != 0 && get_ball_score(2) != 0)
+ end
+
+ def strike(ball)
+ @strike = true
+ add_ball_score(ball, 10)
+ end
+
+ def strike?
+ return true if @strike == true
+ return false
+ end
+
+ def spare
+ @spare = true
+ score = 10 - get_ball_score(1)
+ add_ball_score(2, score)
+ end
+
+ def spare?
+ return true if @spare == true
+ return false
+ end
+
+ def frame_score
+ return @ball_scores.sum
+ end
+
+ def total_frame_score
+ return @ball_scores.sum + @bonus_score
+ end
+end
diff --git a/lib/gameplay.rb b/lib/gameplay.rb
new file mode 100644
index 00000000..10bd6238
--- /dev/null
+++ b/lib/gameplay.rb
@@ -0,0 +1,77 @@
+require_relative 'scorecard'
+
+class Gameplay
+ attr_accessor :current_frame, :current_ball
+
+ def initialize
+ @current_frame = 1
+ @current_ball = 1
+ @scorecard = ScoreCard.new
+ end
+
+ def score_prompt(frames)
+ puts "Enter score for frame #{current_frame}, ball #{current_ball}:"
+ input = gets.chomp
+ input = input.upcase if input.is_a? String
+ return false if validate_input(input, frames) == false
+ input = input.to_i if input != "X" && input != "/"
+ frame = frames[@current_frame]
+ process_current_ball(frame, input)
+ @scorecard.update_pending_bonuses(frames, @current_frame)
+ @scorecard.show_scorecard(frames)
+ return true
+ end
+
+ def process_current_ball(frame, input)
+ if input == "X"
+ frame.strike(@current_ball)
+ elsif input == "/"
+ frame.spare
+ else
+ frame.add_ball_score(@current_ball, input)
+ # binding.irb
+ end
+ end
+
+ def next_ball(frames)
+ if @current_frame < 10
+ if @current_ball == 1 && frames[@current_frame].strike? != true
+ @current_ball = 2
+ else
+ @current_frame = @current_frame + 1
+ @current_ball = 1
+ end
+ else
+ @current_ball = @current_ball + 1
+ end
+ end
+
+ def continue?(frames)
+ frame = frames[@current_frame]
+ # binding.irb
+ if @current_frame == 10 && @current_ball == 4
+ return false
+ elsif @current_frame == 10 && frame.two_balls? == true && frame.spare? == false && frame.strike? == false
+ return false
+ else
+ return true
+ end
+ end
+
+ def final_score(frames)
+ score = 0
+ frames.values.each do |frame|
+ score = score + frame.total_frame_score
+ end
+ puts "\nYour final score is: #{score}"
+ end
+
+ private
+
+ def validate_input(input, frames)
+ return false if input == ""
+ return false if input == "/" && @current_ball == 1
+ return false if input == "X" && @current_ball == 2 && @current_frame != 10
+ return false if /[X\/1-9]/.match(input) == nil
+ end
+end
diff --git a/lib/scorecard.rb b/lib/scorecard.rb
new file mode 100644
index 00000000..9c5cbbd3
--- /dev/null
+++ b/lib/scorecard.rb
@@ -0,0 +1,109 @@
+require_relative 'frame'
+
+class ScoreCard
+ def update_pending_bonuses(frames, current_frame)
+ update_pending_strikes(frames, current_frame)
+ update_pending_spares(frames, current_frame)
+ end
+
+ def show_scorecard(frames)
+ segments = []
+ 9.times do |i|
+ segments << scorecard_segment(frames[i + 1], i + 1)
+ end
+
+ segments << scorecard_segment_frame_10(frames[10])
+
+ lines = []
+
+ 6.times do |i|
+ elements = []
+ 10.times do |l|
+ elements.push(segments[l][i])
+ end
+ lines << elements
+ end
+
+ 6.times do |i|
+ puts lines[i].join
+ end
+ end
+
+ private
+
+ def update_pending_strikes(frames, current_frame)
+ previous_frame = current_frame - 1
+ frame_before_last = current_frame - 2
+
+ return if previous_frame == 0
+ if (frames[current_frame].two_balls?) && (frames[previous_frame].strike?) && (frames[previous_frame].bonus_score == 0)
+ frames[previous_frame].update_bonus_score(frames[current_frame].frame_score)
+ end
+
+ return if frame_before_last == 0
+ if (frames[current_frame].strike?) && (frames[previous_frame].strike?) && (frames[frame_before_last].strike?) && (frames[frame_before_last].bonus_score == 0)
+ frames[frame_before_last].update_bonus_score(20)
+ end
+
+ return if frame_before_last == 0
+ if (frames[current_frame].get_ball_score(1) > 0) && (frames[previous_frame].strike?) && (frames[frame_before_last].strike?) && (frames[frame_before_last].bonus_score == 0)
+ frames[frame_before_last].update_bonus_score(10 + frames[current_frame].frame_score)
+ end
+ end
+
+ def update_pending_spares(frames, current_frame)
+ previous_frame = current_frame - 1
+ return if previous_frame == 0
+
+ if (frames[previous_frame].spare?)
+ frames[previous_frame].update_bonus_score(frames[current_frame].get_ball_score(1))
+ end
+ end
+
+ def scorecard_segment(frame, frame_number)
+ ball_1 = frame.get_ball_score(1)
+ ball_1 = " " if ball_1 == 0
+ ball_1 = "X" if ball_1 == 10
+ ball_1 = ball_1.to_s.ljust(2, " ")
+
+ ball_2 = frame.get_ball_score(2)
+ ball_2 = " " if ball_2 == 0
+ ball_2 = "/" if frame.spare?
+ ball_2 = ball_2.to_s.ljust(2, " ")
+
+ total = frame.total_frame_score
+ total = " " if total == 0
+ total = total.to_s.ljust(3, " ")
+
+ template = [" #{frame_number} ", '┌────┬────┐', '│ 1 │ 2 │', '│ └────┤', '│ TTT │', '└─────────┘']
+ template.map! {|s| s.gsub(/\│ 1 \│ 2 \│/, "│ #{ball_1} │ #{ball_2} │")}
+ template.map! {|s| s.gsub(/\│ TTT \│/, "│ #{total} │")}
+ return template
+ end
+
+ def scorecard_segment_frame_10(frame)
+ ball_1 = frame.get_ball_score(1)
+ ball_1 = " " if ball_1 == 0
+ ball_1 = "X" if ball_1 == 10
+ ball_1 = ball_1.to_s.ljust(2, " ")
+
+ ball_2 = frame.get_ball_score(2)
+ ball_2 = " " if ball_2 == 0
+ ball_2 = "X" if ball_2 == 10
+ ball_2 = ball_2.to_s.ljust(2, " ")
+
+ ball_3 = frame.get_ball_score(3)
+ ball_3 = " " if ball_3 == 0
+ ball_3 = "/" if ball_3 == 10
+ ball_3 = ball_3.to_s.ljust(2, " ")
+
+ total = frame.total_frame_score
+ total = " " if total == 0
+ total = total.to_s.ljust(3, " ")
+
+ template = [" 10 ", "┌────┬────┬────┐", "│ 1 │ 2 │ 3 │", "│ └────┴────┤", "│ TTT │", "└──────────────┘"]
+ template.map! {|s| s.gsub(/\│ 1 \│ 2 \│ 3 \│/, "│ #{ball_1} │ #{ball_2} │ #{ball_3} │")}
+ template.map! {|s| s.gsub(/\│ TTT \│/, "│ #{total} │")}
+ return template
+ end
+end
diff --git a/spec/application_spec.rb b/spec/application_spec.rb
new file mode 100644
index 00000000..03e0a7a8
--- /dev/null
+++ b/spec/application_spec.rb
@@ -0,0 +1,5 @@
+require 'app'
+
+RSpec.describe Application do
+
+end
diff --git a/spec/frame_spec.rb b/spec/frame_spec.rb
new file mode 100644
index 00000000..d6bbdfa9
--- /dev/null
+++ b/spec/frame_spec.rb
@@ -0,0 +1,61 @@
+require 'frame'
+
+RSpec.describe Frame do
+ context ".add_ball_score" do
+ it "adds a ball score to the frame" do
+ frame = Frame.new
+ frame.add_ball_score(1, 5)
+ expect(frame.get_ball_score(1)).to eq 5
+ end
+ end
+
+ context ".get_ball_score" do
+ it "returns the score for a given ball in the frame" do
+ frame = Frame.new
+ frame.add_ball_score(2, 3)
+ expect(frame.get_ball_score(2)).to eq 3
+ end
+ end
+
+ context ".score" do
+ it "returns the score for the frame" do
+ frame = Frame.new
+ frame.add_ball_score(1, 4)
+ frame.add_ball_score(2, 3)
+ expect(frame.frame_score).to eq 7
+ end
+ end
+
+ context ".update_bonus_score" do
+ it "updates the bonus score for the frame" do
+ frame = Frame.new
+ frame.update_bonus_score(5)
+ expect(frame.bonus_score).to eq 5
+ end
+ end
+
+ context ".strike?" do
+ it "returns true if the frame has a strike" do
+ frame = Frame.new
+ frame.strike(1)
+ expect(frame.strike?).to eq true
+ end
+ end
+
+ context ".two_balls?" do
+ it "returns true if the frame has two two balls bowled" do
+ frame = Frame.new
+ frame.add_ball_score(1, 2)
+ frame.add_ball_score(2, 3)
+ expect(frame.two_balls?).to eq true
+ end
+ end
+
+ context ".spare?" do
+ it "returns true if the frame has a spare" do
+ frame = Frame.new
+ frame.spare
+ expect(frame.spare?).to eq true
+ end
+ end
+end
diff --git a/spec/gameplay_spec.rb b/spec/gameplay_spec.rb
new file mode 100644
index 00000000..d30dad98
--- /dev/null
+++ b/spec/gameplay_spec.rb
@@ -0,0 +1,42 @@
+require 'gameplay'
+require 'frame'
+
+RSpec.describe Gameplay do
+ context ".continue?" do
+ it "returns true if the game should continue" do
+ frame_1 = Frame.new
+ frames = { 1 => frame_1 }
+ game = Gameplay.new
+ expect(game.continue?(frames)).to eq true
+ end
+ end
+
+ context ".next_ball" do
+ it "increments the current ball and frame" do
+ frame_1 = Frame.new
+ frame_1.add_ball_score(1, '3')
+ frame_1.add_ball_score(2, '4')
+
+ frame_2 = Frame.new
+ frame_2.strike(1)
+
+ frame_3 = Frame.new
+ frame_3.add_ball_score(1, '2')
+
+ frames = {1 => frame_1, 2 => frame_2, 3 => frame_3}
+
+ game = Gameplay.new
+ expect(game.current_frame).to eq 1
+ expect(game.current_ball).to eq 1
+ game.next_ball(frames)
+ expect(game.current_frame).to eq 1
+ expect(game.current_ball).to eq 2
+ game.next_ball(frames)
+ expect(game.current_frame).to eq 2
+ expect(game.current_ball).to eq 1
+ game.next_ball(frames)
+ expect(game.current_frame).to eq 3
+ expect(game.current_ball).to eq 1
+ end
+ end
+end
diff --git a/spec/scorecard_spec.rb b/spec/scorecard_spec.rb
new file mode 100644
index 00000000..b81206a5
--- /dev/null
+++ b/spec/scorecard_spec.rb
@@ -0,0 +1,108 @@
+require 'scorecard'
+require 'frame'
+
+RSpec.describe ScoreCard do
+
+ context ".update_pending_strikes" do
+ it "updates the bonus for current_frame-1 if two balls have been played in this frame" do
+
+ frame_1 = Frame.new
+ frame_1.strike(1)
+
+ frame_2 = Frame.new
+ frame_2.add_ball_score(1, 3)
+ frame_2.add_ball_score(2, 4)
+
+ frames = {1 => frame_1, 2 => frame_2}
+ current_frame = 2
+
+ score_card = ScoreCard.new
+ score_card.update_pending_bonuses(frames, current_frame)
+
+ expect(frame_1.total_frame_score).to eq 17
+ end
+
+ it "updates the bonus for current_frame-2 if current_frame and current_frame-1 are strikes" do
+
+ frame_1 = Frame.new
+ frame_1.strike(1)
+
+ frame_2 = Frame.new
+ frame_2.strike(1)
+
+ frame_3 = Frame.new
+ frame_3.strike(1)
+
+ frames = {1 => frame_1, 2 => frame_2, 3 => frame_3}
+ current_frame = 3
+ score_card = ScoreCard.new
+ score_card.update_pending_bonuses(frames, current_frame)
+ expect(frame_1.total_frame_score).to eq 30
+ end
+
+ it "updates the bonus for frame 9 if frame 9 had a strike and the first two balls of frame 10 are strikes" do
+ frame_8 = Frame.new
+
+ frame_9 = Frame.new
+ frame_9.strike(1)
+
+ frame_10 = Frame.new
+ frame_10.strike(1)
+ frame_10.strike(2)
+
+ frames = {8 => frame_8, 9 => frame_9, 10 => frame_10}
+ current_frame = 10
+
+ score_card = ScoreCard.new
+ score_card.update_pending_bonuses(frames, current_frame)
+
+ expect(frame_9.total_frame_score).to eq 30
+ end
+
+ it "doesn't apply a bonus if ball 3 of frame 10 is a strike" do
+ frame_8 = Frame.new
+
+ frame_9 = Frame.new
+ frame_9.strike(1)
+
+ frame_10 = Frame.new
+ frame_10.strike(1)
+ frame_10.strike(2)
+
+ frames = {8 => frame_8, 9 => frame_9, 10 => frame_10}
+ current_frame = 10
+
+ score_card = ScoreCard.new
+ score_card.update_pending_bonuses(frames, current_frame)
+
+ expect(frame_9.total_frame_score).to eq 30
+ expect(frame_10.total_frame_score).to eq 20
+
+ frame_10.strike(3)
+
+ score_card.update_pending_bonuses(frames, current_frame)
+
+ expect(frame_9.total_frame_score).to eq 30
+ expect(frame_10.total_frame_score).to eq 30
+ end
+ end
+ context ".update_pending_spares" do
+ it "updates the bonus for current_frame-1 if it had a spare" do
+
+ frame_1 = Frame.new
+ frame_1.add_ball_score(1, 3)
+ frame_1.spare
+
+ frame_2 = Frame.new
+ frame_2.add_ball_score(1, 5)
+
+ frames = {1 => frame_1, 2 => frame_2}
+ current_frame = 2
+
+ score_card = ScoreCard.new
+ score_card.update_pending_bonuses(frames, current_frame)
+
+ expect(frame_1.total_frame_score).to eq 15
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 00000000..066b79aa
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,100 @@
+ENV['ENV'] = 'test'
+
+# This file was generated by the `rspec --init` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+ # have no way to turn it off -- the option exists only for backwards
+ # compatibility in RSpec 3). It causes shared context metadata to be
+ # inherited by the metadata hash of host groups and examples, rather than
+ # triggering implicit auto-inclusion in groups with matching metadata.
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+
+# The settings below are suggested to provide a good initial experience
+# with RSpec, but feel free to customize to your heart's content.
+=begin
+ # This allows you to limit a spec run to individual examples or groups
+ # you care about by tagging them with `:focus` metadata. When nothing
+ # is tagged with `:focus`, all examples get run. RSpec also provides
+ # aliases for `it`, `describe`, and `context` that include `:focus`
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ config.filter_run_when_matching :focus
+
+ # Allows RSpec to persist some state between runs in order to support
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # you configure your source control system to ignore this file.
+ config.example_status_persistence_file_path = "spec/examples.txt"
+
+ # Limits the available syntax to the non-monkey patched syntax that is
+ # recommended. For more details, see:
+ # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
+ config.disable_monkey_patching!
+
+ # This setting enables warnings. It's recommended, but in some cases may
+ # be too noisy due to issues in dependencies.
+ config.warnings = true
+
+ # Many RSpec users commonly either run the entire suite or an individual
+ # file, and it's useful to allow more verbose output when running an
+ # individual spec file.
+ if config.files_to_run.one?
+ # Use the documentation formatter for detailed output,
+ # unless a formatter has already been configured
+ # (e.g. via a command-line flag).
+ config.default_formatter = "doc"
+ end
+
+ # Print the 10 slowest examples and example groups at the
+ # end of the spec run, to help surface which specs are running
+ # particularly slow.
+ config.profile_examples = 10
+
+ # Run specs in random order to surface order dependencies. If you find an
+ # order dependency and want to debug it, you can fix the order by providing
+ # the seed, which is printed after each run.
+ # --seed 1234
+ config.order = :random
+
+ # Seed global randomization in this process using the `--seed` CLI option.
+ # Setting this allows you to use `--seed` to deterministically reproduce
+ # test failures related to randomization by passing the same `--seed` value
+ # as the one that triggered the failure.
+ Kernel.srand config.seed
+=end
+end