games_dice 0.3.12 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +38 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +53 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +18 -0
  7. data/Gemfile +12 -0
  8. data/README.md +11 -33
  9. data/Rakefile +10 -23
  10. data/ext/games_dice/extconf.rb +4 -22
  11. data/ext/games_dice/games_dice.c +1 -1
  12. data/ext/games_dice/probabilities.c +128 -9
  13. data/ext/games_dice/probabilities.h +1 -1
  14. data/games_dice.gemspec +22 -36
  15. data/lib/games_dice/bunch.rb +203 -247
  16. data/lib/games_dice/bunch_helpers.rb +68 -0
  17. data/lib/games_dice/complex_die.rb +151 -270
  18. data/lib/games_dice/complex_die_helpers.rb +260 -60
  19. data/lib/games_dice/constants.rb +10 -10
  20. data/lib/games_dice/dice.rb +153 -143
  21. data/lib/games_dice/die.rb +101 -97
  22. data/lib/games_dice/die_result.rb +206 -189
  23. data/lib/games_dice/map_rule.rb +72 -70
  24. data/lib/games_dice/marshal.rb +41 -13
  25. data/lib/games_dice/parser.rb +245 -218
  26. data/lib/games_dice/reroll_rule.rb +76 -77
  27. data/lib/games_dice/version.rb +4 -1
  28. data/lib/games_dice.rb +23 -25
  29. data/spec/bunch_spec.rb +399 -420
  30. data/spec/complex_die_spec.rb +314 -305
  31. data/spec/dice_spec.rb +33 -34
  32. data/spec/die_result_spec.rb +174 -181
  33. data/spec/die_spec.rb +81 -81
  34. data/spec/helpers.rb +25 -25
  35. data/spec/map_rule_spec.rb +40 -44
  36. data/spec/parser_spec.rb +106 -82
  37. data/spec/probability_spec.rb +522 -526
  38. data/spec/readme_spec.rb +410 -390
  39. data/spec/reroll_rule_spec.rb +40 -44
  40. metadata +17 -129
  41. data/.travis.yml +0 -14
  42. data/lib/games_dice/prob_helpers.rb +0 -259
  43. data/lib/games_dice/probabilities.rb +0 -244
@@ -1,97 +1,101 @@
1
- # This class models the simplest, most-familiar kind of die.
2
- #
3
- # An object of the class represents a basic die that rolls 1..#sides, with equal weighting for each value.
4
- #
5
- # @example Create a 6-sided die, and roll it
6
- # d = GamesDice::Die.new( 6 )
7
- # d.roll # => Integer in range 1..6
8
- # d.result # => same Integer value as just returned by d.roll
9
- #
10
- # @example Create a 10-sided die, that rolls using a monkey-patch to SecureRandom
11
- # module SecureRandom
12
- # def self.rand n
13
- # random_number( n )
14
- # end
15
- # end
16
- # d = GamesDice::Die.new( 10, SecureRandom )
17
- # d.roll # => (secure) Integer in range 1..10
18
- # d.result # => same Integer value as just returned by d.roll
19
-
20
- class GamesDice::Die
21
-
22
- # Creates new instance of GamesDice::Die
23
- # @param [Integer] sides the number of sides
24
- # @param [#rand] prng random number generator, GamesDice::Die will use Ruby's built-in #rand() by default
25
- # @return [GamesDice::Die]
26
- def initialize( sides, prng=nil )
27
- @sides = Integer(sides)
28
- raise ArgumentError, "sides value #{sides} is too low, it must be 1 or greater" if @sides < 1
29
- raise ArgumentError, "prng does not support the rand() method" if prng && ! prng.respond_to?(:rand)
30
- @prng = prng
31
- @result = nil
32
- end
33
-
34
- # @return [Integer] number of sides on simulated die
35
- attr_reader :sides
36
-
37
- # @return [Integer] result of last call to #roll, nil if no call made yet
38
- attr_reader :result
39
-
40
- # @return [Object] random number generator as supplied to constructor, may be nil
41
- attr_reader :prng
42
-
43
- # @!attribute [r] min
44
- # @return [Integer] minimum possible result from a call to #roll
45
- def min
46
- 1
47
- end
48
-
49
- # @!attribute [r] max
50
- # @return [Integer] maximum possible result from a call to #roll
51
- def max
52
- @sides
53
- end
54
-
55
- # Calculates probability distribution for this die.
56
- # @return [GamesDice::Probabilities] probability distribution of the die
57
- def probabilities
58
- @probabilities ||= GamesDice::Probabilities.for_fair_die( @sides )
59
- end
60
-
61
- # Simulates rolling the die
62
- # @return [Integer] selected value between 1 and #sides inclusive
63
- def roll
64
- if @prng
65
- @result = @prng.rand(@sides) + 1
66
- else
67
- @result = rand(@sides) + 1
68
- end
69
- end
70
-
71
- # Iterates through all possible results on die.
72
- # @yieldparam [Integer] result A potential result from the die
73
- # @return [GamesDice::Die] this object
74
- def each_value
75
- (1..@sides).each { |r| yield(r) }
76
- self
77
- end
78
-
79
- # @return [Array<Integer>] All potential results from the die
80
- def all_values
81
- (1..@sides).to_a
82
- end
83
-
84
- # @!attribute [r] rerolls
85
- # Rules for when to re-roll this die.
86
- # @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
87
- def rerolls
88
- nil
89
- end
90
-
91
- # @!attribute [r] maps
92
- # Rules for when to map return value of this die.
93
- # @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
94
- def maps
95
- nil
96
- end
97
- end # class GamesDice::Die
1
+ # frozen_string_literal: true
2
+
3
+ module GamesDice
4
+ # This class models the simplest, most-familiar kind of die.
5
+ #
6
+ # An object of the class represents a basic die that rolls 1..#sides, with equal weighting for each value.
7
+ #
8
+ # @example Create a 6-sided die, and roll it
9
+ # d = GamesDice::Die.new( 6 )
10
+ # d.roll # => Integer in range 1..6
11
+ # d.result # => same Integer value as just returned by d.roll
12
+ #
13
+ # @example Create a 10-sided die, that rolls using a monkey-patch to SecureRandom
14
+ # module SecureRandom
15
+ # def self.rand n
16
+ # random_number( n )
17
+ # end
18
+ # end
19
+ # d = GamesDice::Die.new( 10, SecureRandom )
20
+ # d.roll # => (secure) Integer in range 1..10
21
+ # d.result # => same Integer value as just returned by d.roll
22
+ #
23
+ class Die
24
+ # Creates new instance of GamesDice::Die
25
+ # @param [Integer] sides the number of sides
26
+ # @param [#rand] prng random number generator, GamesDice::Die will use Ruby's built-in #rand() by default
27
+ # @return [GamesDice::Die]
28
+ def initialize(sides, prng = nil)
29
+ @sides = Integer(sides)
30
+ raise ArgumentError, "sides value #{sides} is too low, it must be 1 or greater" if @sides < 1
31
+ raise ArgumentError, 'prng does not support the rand() method' if prng && !prng.respond_to?(:rand)
32
+
33
+ @prng = prng
34
+ @result = nil
35
+ end
36
+
37
+ # @return [Integer] number of sides on simulated die
38
+ attr_reader :sides
39
+
40
+ # @return [Integer] result of last call to #roll, nil if no call made yet
41
+ attr_reader :result
42
+
43
+ # @return [Object] random number generator as supplied to constructor, may be nil
44
+ attr_reader :prng
45
+
46
+ # @!attribute [r] min
47
+ # @return [Integer] minimum possible result from a call to #roll
48
+ def min
49
+ 1
50
+ end
51
+
52
+ # @!attribute [r] max
53
+ # @return [Integer] maximum possible result from a call to #roll
54
+ def max
55
+ @sides
56
+ end
57
+
58
+ # Calculates probability distribution for this die.
59
+ # @return [GamesDice::Probabilities] probability distribution of the die
60
+ def probabilities
61
+ @probabilities ||= GamesDice::Probabilities.for_fair_die(@sides)
62
+ end
63
+
64
+ # Simulates rolling the die
65
+ # @return [Integer] selected value between 1 and #sides inclusive
66
+ def roll
67
+ @result = if @prng
68
+ @prng.rand(@sides) + 1
69
+ else
70
+ rand(@sides) + 1
71
+ end
72
+ end
73
+
74
+ # Iterates through all possible results on die.
75
+ # @yieldparam [Integer] result A potential result from the die
76
+ # @return [GamesDice::Die] this object
77
+ def each_value(&)
78
+ (1..@sides).each(&)
79
+ self
80
+ end
81
+
82
+ # @return [Array<Integer>] All potential results from the die
83
+ def all_values
84
+ (1..@sides).to_a
85
+ end
86
+
87
+ # @!attribute [r] rerolls
88
+ # Rules for when to re-roll this die.
89
+ # @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
90
+ def rerolls
91
+ nil
92
+ end
93
+
94
+ # @!attribute [r] maps
95
+ # Rules for when to map return value of this die.
96
+ # @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
97
+ def maps
98
+ nil
99
+ end
100
+ end
101
+ end
@@ -1,189 +1,206 @@
1
- # This class models the output of GamesDice::ComplexDie.
2
- #
3
- # An object of the class represents the results of a roll of a ComplexDie, including any re-rolls and
4
- # value mapping.
5
- #
6
- # @example Building up a result manually
7
- # dr = GamesDice::DieResult.new
8
- # dr.add_roll 5
9
- # dr.add_roll 4, :reroll_replace
10
- # dr.value # => 4
11
- # dr.rolls # => [5, 4]
12
- # dr.roll_reasons # => [:basic, :reroll_replace]
13
- # # dr can behave as dr.value due to coercion and support for some operators
14
- # dr + 6 # => 10
15
- #
16
- # @example Using a result from GamesDice::ComplexDie
17
- # # An "exploding" six-sided die that needs a result of 8 to score "1 Success"
18
- # d = GamesDice::ComplexDie.new( 6, :rerolls => [[6, :<=, :reroll_add]], :maps => [[8, :<=, 1, 'Success']] )
19
- # # Generate result object by rolling the die
20
- # dr = d.roll
21
- # dr.rolls # => [6, 3]
22
- # dr.roll_reasons # => [:basic, :reroll_add]
23
- # dr.total # => 9
24
- # dr.value # => 1
25
- # dr.explain_value # => "[6+3] 9 Success"
26
- #
27
-
28
- class GamesDice::DieResult
29
- include Comparable
30
-
31
- # Creates new instance of GamesDice::DieResult. The object can be initialised "empty" or with a first result.
32
- # @param [Integer,nil] first_roll_result Value for first roll of the die.
33
- # @param [Symbol] first_roll_reason Reason for first roll of the die.
34
- # @return [GamesDice::DieResult]
35
- def initialize( first_roll_result=nil, first_roll_reason=:basic )
36
- unless GamesDice::REROLL_TYPES.has_key?(first_roll_reason)
37
- raise ArgumentError, "Unrecognised reason for roll #{first_roll_reason}"
38
- end
39
-
40
- if (first_roll_result)
41
- @rolls = [Integer(first_roll_result)]
42
- @roll_reasons = [first_roll_reason]
43
- @total = @rolls[0]
44
- else
45
- @rolls = []
46
- @roll_reasons = []
47
- @total = nil
48
- end
49
- @mapped = false
50
- @value = @total
51
- end
52
-
53
- # The individual die rolls that combined to generate this result.
54
- # @return [Array<Integer>] Un-processed values of each die roll used for this result.
55
- attr_reader :rolls
56
-
57
- # The individual reasons for each roll of the die. See GamesDice::RerollRule for allowed values.
58
- # @return [Array<Symbol>] Reasons for each die roll, indexes match the #rolls Array.
59
- attr_reader :roll_reasons
60
-
61
- # Combined result of all rolls, *before* mapping.
62
- # @return [Integer,nil]
63
- attr_reader :total
64
-
65
- # Combined result of all rolls, *after* mapping.
66
- # @return [Integer,nil]
67
- attr_reader :value
68
-
69
- # Whether or not #value has been mapped from #total.
70
- # @return [Boolean]
71
- attr_reader :mapped
72
-
73
- # Adds value from a new roll to the object. GamesDice::DieResult tracks reasons for the roll
74
- # and makes the correct adjustment to the total so far. Any mapped value is cleared.
75
- # @param [Integer] roll_result Value result from rolling the die.
76
- # @param [Symbol] roll_reason Reason for rolling the die.
77
- # @return [Integer] Total so far
78
- def add_roll( roll_result, roll_reason=:basic )
79
- unless GamesDice::REROLL_TYPES.has_key?(roll_reason)
80
- raise ArgumentError, "Unrecognised reason for roll #{roll_reason}"
81
- end
82
- @rolls << Integer(roll_result)
83
- @roll_reasons << roll_reason
84
- if @rolls.length == 1
85
- @total = 0
86
- end
87
-
88
- case roll_reason
89
- when :basic
90
- @total = roll_result
91
- when :reroll_add
92
- @total += roll_result
93
- when :reroll_subtract
94
- @total -= roll_result
95
- when :reroll_new_die
96
- @total = roll_result
97
- when :reroll_new_keeper
98
- @total = roll_result
99
- when :reroll_replace
100
- @total = roll_result
101
- when :reroll_use_best
102
- @total = [@value,roll_result].max
103
- when :reroll_use_worst
104
- @total = [@value,roll_result].min
105
- end
106
-
107
- @mapped = false
108
- @value = @total
109
- end
110
-
111
- # Sets value arbitrarily, and notes that the value has been mapped. Used by GamesDice::ComplexDie
112
- # when there are one or more GamesDice::MapRule objects to process for a die.
113
- # @param [Integer] to_value Replacement value.
114
- # @param [String] description Description of what the mapped value represents e.g. "Success"
115
- # @return [nil]
116
- def apply_map( to_value, description = '' )
117
- @mapped = true
118
- @value = to_value
119
- @map_description = description
120
- return
121
- end
122
-
123
- # Generates a text description of how #value is determined. If #value has been mapped, includes the
124
- # map description, but does not include the mapped value.
125
- # @return [String] Explanation of #value.
126
- def explain_value
127
- text = ''
128
- if @rolls.length < 2
129
- text = @total.to_s
130
- else
131
- text = '[' + @rolls[0].to_s
132
- text = (1..@rolls.length-1).inject( text ) { |so_far,i| so_far + GamesDice::REROLL_TYPES[@roll_reasons[i]] + @rolls[i].to_s }
133
- text += '] ' + @total.to_s
134
- end
135
- text += ' ' + @map_description if @mapped && @map_description && @map_description.length > 0
136
- return text
137
- end
138
-
139
- # @!visibility private
140
- # This is mis-named, it doesn't explain the total at all! It is used to generate summaries of keeper dice.
141
- def explain_total
142
- text = @total.to_s
143
- text += ' ' + @map_description if @mapped && @map_description && @map_description.length > 0
144
- return text
145
- end
146
-
147
- # @!visibility private
148
- # all coercions simply use #value (i.e. nil or a Integer)
149
- def coerce(thing)
150
- @value.coerce(thing)
151
- end
152
-
153
- # @!visibility private
154
- # addition uses #value
155
- def +(thing)
156
- @value + thing
157
- end
158
-
159
- # @!visibility private
160
- # subtraction uses #value
161
- def -(thing)
162
- @value - thing
163
- end
164
-
165
- # @!visibility private
166
- # multiplication uses #value
167
- def *(thing)
168
- @value * thing
169
- end
170
-
171
- # @!visibility private
172
- # comparison <=> uses #value
173
- def <=>(other)
174
- self.value <=> other
175
- end
176
-
177
- # This is a deep clone, all attributes are cloned.
178
- # @return [GamesDice::DieResult]
179
- def clone
180
- cloned = GamesDice::DieResult.new()
181
- cloned.instance_variable_set('@rolls', @rolls.clone)
182
- cloned.instance_variable_set('@roll_reasons', @roll_reasons.clone)
183
- cloned.instance_variable_set('@total', @total)
184
- cloned.instance_variable_set('@value', @value)
185
- cloned.instance_variable_set('@mapped', @mapped)
186
- cloned.instance_variable_set('@map_description', @map_description)
187
- cloned
188
- end
189
- end
1
+ # frozen_string_literal: true
2
+
3
+ module GamesDice
4
+ # Module supports using DieResults directly in calculations
5
+ # @!visibility private
6
+ module ExpressionHelpers
7
+ # all coercions simply use #value (i.e. nil or a Integer)
8
+ def coerce(thing)
9
+ @value.coerce(thing)
10
+ end
11
+
12
+ # addition uses #value
13
+ def +(other)
14
+ @value + other
15
+ end
16
+
17
+ # subtraction uses #value
18
+ def -(other)
19
+ @value - other
20
+ end
21
+
22
+ # multiplication uses #value
23
+ def *(other)
24
+ @value * other
25
+ end
26
+
27
+ # comparison <=> uses #value
28
+ def <=>(other)
29
+ value <=> other
30
+ end
31
+ end
32
+
33
+ # This class models the output of GamesDice::ComplexDie.
34
+ #
35
+ # An object of the class represents the results of a roll of a ComplexDie, including any re-rolls and
36
+ # value mapping.
37
+ #
38
+ # @example Building up a result manually
39
+ # dr = GamesDice::DieResult.new
40
+ # dr.add_roll 5
41
+ # dr.add_roll 4, :reroll_replace
42
+ # dr.value # => 4
43
+ # dr.rolls # => [5, 4]
44
+ # dr.roll_reasons # => [:basic, :reroll_replace]
45
+ # # dr can behave as dr.value due to coercion and support for some operators
46
+ # dr + 6 # => 10
47
+ #
48
+ # @example Using a result from GamesDice::ComplexDie
49
+ # # An "exploding" six-sided die that needs a result of 8 to score "1 Success"
50
+ # d = GamesDice::ComplexDie.new( 6, :rerolls => [[6, :<=, :reroll_add]], :maps => [[8, :<=, 1, 'Success']] )
51
+ # # Generate result object by rolling the die
52
+ # dr = d.roll
53
+ # dr.rolls # => [6, 3]
54
+ # dr.roll_reasons # => [:basic, :reroll_add]
55
+ # dr.total # => 9
56
+ # dr.value # => 1
57
+ # dr.explain_value # => "[6+3] 9 Success"
58
+ #
59
+ class DieResult
60
+ include Comparable
61
+ include ExpressionHelpers
62
+
63
+ # Creates new instance of GamesDice::DieResult. The object can be initialised "empty" or with a first result.
64
+ # @param [Integer,nil] first_roll_result Value for first roll of the die.
65
+ # @param [Symbol] first_roll_reason Reason for first roll of the die.
66
+ # @return [GamesDice::DieResult]
67
+ def initialize(first_roll_result = nil, first_roll_reason = :basic)
68
+ unless GamesDice::REROLL_TYPES.key?(first_roll_reason)
69
+ raise ArgumentError, "Unrecognised reason for roll #{first_roll_reason}"
70
+ end
71
+
72
+ if first_roll_result
73
+ init_with_result(first_roll_result, first_roll_reason)
74
+ else
75
+ init_empty
76
+ end
77
+ @mapped = false
78
+ @value = @total
79
+ end
80
+
81
+ # The individual die rolls that combined to generate this result.
82
+ # @return [Array<Integer>] Un-processed values of each die roll used for this result.
83
+ attr_reader :rolls
84
+
85
+ # The individual reasons for each roll of the die. See GamesDice::RerollRule for allowed values.
86
+ # @return [Array<Symbol>] Reasons for each die roll, indexes match the #rolls Array.
87
+ attr_reader :roll_reasons
88
+
89
+ # Combined result of all rolls, *before* mapping.
90
+ # @return [Integer,nil]
91
+ attr_reader :total
92
+
93
+ # Combined result of all rolls, *after* mapping.
94
+ # @return [Integer,nil]
95
+ attr_reader :value
96
+
97
+ # Whether or not #value has been mapped from #total.
98
+ # @return [Boolean]
99
+ attr_reader :mapped
100
+
101
+ # Adds value from a new roll to the object. GamesDice::DieResult tracks reasons for the roll
102
+ # and makes the correct adjustment to the total so far. Any mapped value is cleared.
103
+ # @param [Integer] roll_result Value result from rolling the die.
104
+ # @param [Symbol] roll_reason Reason for rolling the die.
105
+ # @return [Integer] Total so far
106
+ def add_roll(roll_result, roll_reason = :basic)
107
+ raise ArgumentError, "Unrecognised roll reason #{roll_reason}" unless GamesDice::REROLL_TYPES.key?(roll_reason)
108
+
109
+ @rolls << Integer(roll_result)
110
+ @roll_reasons << roll_reason
111
+ @total = 0 if @rolls.length == 1
112
+
113
+ apply_roll_to_total(roll_reason, roll_result)
114
+
115
+ @mapped = false
116
+ @value = @total
117
+ end
118
+
119
+ # Sets value arbitrarily, and notes that the value has been mapped. Used by GamesDice::ComplexDie
120
+ # when there are one or more GamesDice::MapRule objects to process for a die.
121
+ # @param [Integer] to_value Replacement value.
122
+ # @param [String] description Description of what the mapped value represents e.g. "Success"
123
+ # @return [nil]
124
+ def apply_map(to_value, description = '')
125
+ @mapped = true
126
+ @value = to_value
127
+ @map_description = description
128
+ nil
129
+ end
130
+
131
+ # Generates a text description of how #value is determined. If #value has been mapped, includes the
132
+ # map description, but does not include the mapped value.
133
+ # @return [String] Explanation of #value.
134
+ def explain_value
135
+ text = if @rolls.length < 2
136
+ @total.to_s
137
+ else
138
+ explain_value_multiple_rolls
139
+ end
140
+ text += " #{@map_description}" if @mapped && @map_description&.length&.positive?
141
+ text
142
+ end
143
+
144
+ # @!visibility private
145
+ # This is mis-named, it doesn't explain the total at all! It is used to generate summaries of keeper dice.
146
+ def explain_total
147
+ text = @total.to_s
148
+ text += " #{@map_description}" if @mapped && @map_description&.length&.positive?
149
+ text
150
+ end
151
+
152
+ # This is a deep clone, all attributes are also cloned.
153
+ # @return [GamesDice::DieResult]
154
+ def clone
155
+ cloned = GamesDice::DieResult.new
156
+ cloned.instance_variable_set(:@rolls, @rolls.clone)
157
+ cloned.instance_variable_set(:@roll_reasons, @roll_reasons.clone)
158
+ cloned.instance_variable_set(:@total, @total)
159
+ cloned.instance_variable_set(:@value, @value)
160
+ cloned.instance_variable_set(:@mapped, @mapped)
161
+ cloned.instance_variable_set(:@map_description, @map_description)
162
+ cloned
163
+ end
164
+
165
+ private
166
+
167
+ # Splitting this method up further, or flattening it will not make it read better. If
168
+ # we had a few more reroll reasons, they could maybe be grouped and method split up.
169
+ # rubocop:disable Metrics/MethodLength
170
+ def apply_roll_to_total(roll_reason, roll_result)
171
+ case roll_reason
172
+ when :basic, :reroll_new_die, :reroll_new_keeper, :reroll_replace
173
+ @total = roll_result
174
+ when :reroll_add
175
+ @total += roll_result
176
+ when :reroll_subtract
177
+ @total -= roll_result
178
+ when :reroll_use_best
179
+ @total = [@value, roll_result].max
180
+ when :reroll_use_worst
181
+ @total = [@value, roll_result].min
182
+ end
183
+ end
184
+ # rubocop:enable Metrics/MethodLength
185
+
186
+ def init_with_result(first_roll_result, first_roll_reason)
187
+ @rolls = [Integer(first_roll_result)]
188
+ @roll_reasons = [first_roll_reason]
189
+ @total = @rolls[0]
190
+ end
191
+
192
+ def init_empty
193
+ @rolls = []
194
+ @roll_reasons = []
195
+ @total = nil
196
+ end
197
+
198
+ def explain_value_multiple_rolls
199
+ text = "[#{@rolls[0]}"
200
+ text = (1..(@rolls.length - 1)).inject(text) do |so_far, i|
201
+ so_far + GamesDice::REROLL_TYPES[@roll_reasons[i]] + @rolls[i].to_s
202
+ end
203
+ text + "] #{@total}"
204
+ end
205
+ end
206
+ end