bowling.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
class Frame
  MAX_SCORE = 10

  def initialize(prev)
    @prev = prev
    @bonus = 0
    @rolls = []
  end

  def roll_total
    # Total roll value for this frame
    @rolls.inject(0) { |a, e| a + e }
  end

  def add_bonus(n)
    # Add a bonus score to this frame
    @bonus += n
  end

  def score
    # Total score for this frame (rolls + bonuses)
    roll_total + @bonus
  end

  def complete?
    # Is this frame complete?
    # It's complete if it's had two rolls, or a strike was scored.
    @rolls.count == 2 || strike?
  end

  def strike?
    # Was this a strike?
    @rolls.first == MAX_SCORE
  end

  def spare?
    # Was this a spare?
    @rolls.count == 2 && roll_total == MAX_SCORE
  end

  def open?
    # Was this an open frame?
    roll_total < MAX_SCORE
  end

  def roll(n)
    # Roll n pins in this frame
    raise 'Pin count exceeds pins on the lane' if roll_total + n > 10

    @rolls << n

    apply_bonuses(n)
  end

  attr_reader :prev

  private

  def apply_strike(n)
    # If we just got a strike, the last frame gets this score as a bonus.
    @prev.add_bonus(n)

    # If we got two strikes in a row, the last-but-one frame gets the
    # first roll of this frame as a bonus
    if @prev.prev && @prev.prev.strike? && @rolls.count == 1
      @prev.prev.add_bonus(n)
    end
  end

  def apply_bonuses(n)
    # Apply bonus scores to previous frames
    if @prev
      if @prev.strike?
        apply_strike(n)
      elsif @prev.spare? && @rolls.count == 1
        # If we just got a spare,
        # the last frame gets the first roll of this frame as a bonus
        @prev.add_bonus(n)
      end
    end
  end
end

class FillFrame < Frame
  def complete?
    # A fill frame is considered complete if the last one was spare
    # (a second roll is not needed)
    super || @prev.spare?
  end

  def score
    # Fill frames don't score (they give their rolls to previous frames)
    0
  end
end

class Game
  VERSION = 1
  MAX_FRAMES = 10
  MAX_FRAMES_WITH_FILL = 12

  def initialize
    @frame = Frame.new(nil)
    @frames = [@frame]
  end

  def roll(n)
    raise 'Pins must have a value from 0 to 10' unless (0..10).cover? n
    raise 'Should not be able to roll after game is over' if game_over?

    # Start a new frame if the previous one is complete
    if @frame.complete?
      cls = @frames.count < MAX_FRAMES ? Frame : FillFrame
      @frame = cls.new(@frame)
      @frames << @frame
    end

    @frame.roll(n)
  end

  def game_over?
    # are all frames complete?
    all_complete = @frames.all?(&:complete?)
    # Have there been enough frames?
    # If there have, the last one must be open
    # Unless we've got to 12, the absolute max.
    enough_frames = (
      (@frames.count >= MAX_FRAMES && @frames.last.open?) ||
      @frames.count == MAX_FRAMES_WITH_FILL
    )
    all_complete && enough_frames
  end

  def score
    raise 'Score cannot be taken until the end of the game' unless game_over?

    @frames.inject(0) { |a, e| a + e.score }
  end
end

Comments

This took forever!

helenst commented 9 May 2016 at 10:00 UTC

This is a complicated one with tons of state! I love your comments here, I've been trying to add more for my solutions as well. Likewise the Frame/FinalFrame split.

In Frame#roll_total you can compact that a little as well as use the name more common in other languages: @rolls.reduce(0, &:+)

Having the frames carry a dependency on the previous frame is an interesting solution to the scoring problem! Does it feel like it separates things cleanly, though? Each of these objects is reaching around into other ones to do some dirty work. I took the approach of scoring the game backwards, that way each frame is described as "my score + the previous 0-2 rolls" depending on what was rolled.

alialliallie commented 9 May 2016 at 18:10 UTC

Thanks for taking the time to read through and comment on this! It feels OK I guess - I might give it another look. Maybe it's better to just keep roll values in the frame and then bring scoring up to the game level so frames don't need to know about one another.

Thanks for the advice for compacting too - I need to make more use of that! :)

helenst commented 10 May 2016 at 12:02 UTC

After writing in other languages I really got used to function literals, and kept wishing for them in Ruby. In some cases like this reduce(&:+) works, or reduce(&method(:some_method)) but with multiple arguments and currying it gets funky.

If you're ok with it, that's great! Basically a linked list of Frames, yeah?

alialliallie commented 10 May 2016 at 21:31 UTC

You're not logged in right now. Please login via GitHub to comment