games_dice 0.4.0 → 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.
@@ -1,68 +1,260 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # @!visibility private
4
3
  module GamesDice
5
- # Private extension methods for GamesDice::ComplexDie
6
- module ComplexDieHelpers
7
- private
4
+ class ComplexDie
5
+ # @!visibility private
6
+ # Private extension methods for GamesDice::ComplexDie probability calculations
7
+ module ProbabilityHelpers
8
+ private
8
9
 
9
- def recursive_probabilities(probabilities = {}, prior_probability = 1.0, depth = 0, prior_result = nil, rerolls_left = nil, roll_reason = :basic, subtracting = false)
10
- each_probability = prior_probability / @basic_die.sides
11
- depth += 1
12
- if depth >= 20 || each_probability < 1.0e-16
13
- @probabilities_complete = false
14
- stop_recursing = true
10
+ def calculate_probabilities
11
+ if @rerolls && @maps
12
+ GamesDice::Probabilities.from_h(prob_hash_with_rerolls_and_maps)
13
+ elsif @rerolls
14
+ GamesDice::Probabilities.from_h(recursive_probabilities)
15
+ elsif @maps
16
+ GamesDice::Probabilities.from_h(prob_hash_with_just_maps)
17
+ else
18
+ @basic_die.probabilities
19
+ end
15
20
  end
16
21
 
17
- @basic_die.each_value do |v|
18
- recurse_probs_for_value(v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left,
19
- subtracting, stop_recursing)
22
+ def prob_hash_with_rerolls_and_maps
23
+ prob_hash = {}
24
+ reroll_probs = recursive_probabilities
25
+ reroll_probs.each do |v, p|
26
+ add_mapped_to_prob_hash(prob_hash, v, p)
27
+ end
28
+ prob_hash
20
29
  end
21
- probabilities
22
- end
23
30
 
24
- def recurse_probs_for_value(v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left, subtracting, stop_recursing)
25
- # calculate value, recurse if there is a reroll
26
- result_so_far, rerolls_remaining = calc_result_so_far(prior_result, rerolls_left, v, roll_reason)
31
+ def prob_hash_with_just_maps
32
+ prob_hash = {}
33
+ @basic_die.probabilities.each do |v, p|
34
+ add_mapped_to_prob_hash(prob_hash, v, p)
35
+ end
36
+ prob_hash
37
+ end
27
38
 
28
- # Find which rule, if any, is being triggered
29
- rule_idx = find_matching_reroll_rule(v, result_so_far.rolls.length, rerolls_remaining)
39
+ def add_mapped_to_prob_hash(prob_hash, orig_val, prob)
40
+ mapped_val, = calc_maps(orig_val)
41
+ prob_hash[mapped_val] ||= 0.0
42
+ prob_hash[mapped_val] += prob
43
+ end
44
+
45
+ RecurseStack = Struct.new(:depth, :roll_reason, :subtracting, :probabilities, :prior_probability, :prior_result,
46
+ :rerolls_left) do
47
+ def initialize
48
+ self.depth = 0
49
+ self.roll_reason = :basic
50
+ self.subtracting = false
51
+ self.probabilities = {}
52
+ self.prior_probability = 1.0
53
+ end
54
+ end
55
+ private_constant :RecurseStack
56
+
57
+ def recursive_probabilities(stack = RecurseStack.new)
58
+ stack.prior_probability = stack.prior_probability / @basic_die.sides
59
+ stack.depth += 1
60
+
61
+ @basic_die.each_value do |die_val|
62
+ recurse_probs_for_value(die_val, stack)
63
+ end
64
+ stack.probabilities
65
+ end
66
+
67
+ def recurse_probs_for_value(die_val, stack)
68
+ result_so_far, rerolls_remaining = calc_result_so_far(die_val, stack)
69
+ rule_idx = find_matching_reroll_rule(die_val, result_so_far.rolls.length, rerolls_remaining)
70
+
71
+ if conintue_recursing?(stack, rule_idx)
72
+ continue_recursion(stack, result_so_far, rerolls_remaining, rule_idx)
73
+ else
74
+ end_recursion_store_probs(stack, result_so_far)
75
+ end
76
+ end
77
+
78
+ def conintue_recursing?(stack, rule_idx)
79
+ if stack.depth >= 20 || stack.prior_probability < 1.0e-16
80
+ @probabilities_complete = false
81
+ return false
82
+ end
83
+
84
+ !rule_idx.nil?
85
+ end
30
86
 
31
- if rule_idx && !stop_recursing
32
- recurse_probs_with_rule(probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx,
33
- subtracting)
34
- else
87
+ def continue_recursion(stack, result_so_far, rerolls_remaining, rule_idx)
88
+ rule = @rerolls[rule_idx]
89
+ rerolls_remaining[rule_idx] -= 1
90
+ recurse_probs_with_rule(stack, result_so_far, rerolls_remaining, rule)
91
+ end
92
+
93
+ def end_recursion_store_probs(stack, result_so_far)
35
94
  t = result_so_far.total
36
- probabilities[t] ||= 0.0
37
- probabilities[t] += each_probability
95
+ stack.probabilities[t] ||= 0.0
96
+ stack.probabilities[t] += stack.prior_probability
38
97
  end
39
- end
40
98
 
41
- def recurse_probs_with_rule(probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx, subtracting)
42
- rule = @rerolls[rule_idx]
43
- rerolls_remaining[rule_idx] -= 1
44
- is_subtracting = true if subtracting || rule.type == :reroll_subtract
99
+ def recurse_probs_with_rule(stack, result_so_far, rerolls_remaining, rule)
100
+ next_stack = stack.clone
101
+ next_stack.prior_result = result_so_far
102
+ next_stack.rerolls_left = rerolls_remaining
103
+ next_stack.subtracting = true if stack.subtracting || rule.type == :reroll_subtract
104
+
105
+ # Apply the rule (note reversal for additions, after a subtract)
106
+ next_stack.roll_reason = if stack.subtracting && rule.type == :reroll_add
107
+ :reroll_subtract
108
+ else
109
+ rule.type
110
+ end
45
111
 
46
- # Apply the rule (note reversal for additions, after a subtract)
47
- if subtracting && rule.type == :reroll_add
48
- recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining,
49
- :reroll_subtract, is_subtracting
50
- else
51
- recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule.type,
52
- is_subtracting
112
+ recursive_probabilities next_stack
113
+ end
114
+
115
+ def calc_result_so_far(die_val, stack)
116
+ if stack.prior_result
117
+ result_so_far = stack.prior_result.clone
118
+ rerolls_remaining = stack.rerolls_left.clone
119
+ result_so_far.add_roll(die_val, stack.roll_reason)
120
+ else
121
+ rerolls_remaining = @rerolls.map(&:limit)
122
+ result_so_far = GamesDice::DieResult.new(die_val, stack.roll_reason)
123
+ end
124
+ [result_so_far, rerolls_remaining]
53
125
  end
54
126
  end
55
127
 
56
- def calc_result_so_far(prior_result, rerolls_left, v, roll_reason)
57
- if prior_result
58
- result_so_far = prior_result.clone
59
- result_so_far.add_roll(v, roll_reason)
60
- rerolls_remaining = rerolls_left.clone
61
- else
62
- result_so_far = GamesDice::DieResult.new(v, roll_reason)
128
+ # @!visibility private
129
+ # Private extension methods for GamesDice::ComplexDie simulating rolls
130
+ module RollHelpers
131
+ private
132
+
133
+ def roll_apply_rerolls
134
+ return unless @rerolls
135
+
136
+ subtracting = false
63
137
  rerolls_remaining = @rerolls.map(&:limit)
138
+
139
+ rerolls_loop(subtracting, rerolls_remaining)
140
+ end
141
+
142
+ def rerolls_loop(subtracting, rerolls_remaining)
143
+ loop do
144
+ rule_idx = find_matching_reroll_rule(@basic_die.result, @result.rolls.length, rerolls_remaining)
145
+ break unless rule_idx
146
+
147
+ rule = @rerolls[rule_idx]
148
+ rerolls_remaining[rule_idx] -= 1
149
+ subtracting = true if rule.type == :reroll_subtract
150
+ roll_apply_reroll_rule rule, subtracting
151
+ end
152
+ end
153
+
154
+ def roll_apply_reroll_rule(rule, is_subtracting)
155
+ # Apply the rule (note reversal for additions, after a subtract)
156
+ if is_subtracting && rule.type == :reroll_add
157
+ @result.add_roll(@basic_die.roll, :reroll_subtract)
158
+ else
159
+ @result.add_roll(@basic_die.roll, rule.type)
160
+ end
161
+ end
162
+
163
+ # Find which rule, if any, is being triggered
164
+ def find_matching_reroll_rule(check_value, num_rolls, rerolls_remaining)
165
+ @rerolls.zip(rerolls_remaining).find_index do |rule, remaining|
166
+ next if rule.type == :reroll_subtract && num_rolls > 1
167
+
168
+ remaining.positive? && rule.applies?(check_value)
169
+ end
170
+ end
171
+
172
+ def roll_apply_maps
173
+ return unless @maps
174
+
175
+ m, n = calc_maps(@result.value)
176
+ @result.apply_map(m, n)
177
+ end
178
+
179
+ def calc_maps(original_value)
180
+ y = 0
181
+ n = ''
182
+ @maps.find do |rule|
183
+ if (maybe_y = rule.map_from(original_value))
184
+ y = maybe_y
185
+ n = rule.mapped_name
186
+ end
187
+ maybe_y
188
+ end
189
+ [y, n]
190
+ end
191
+ end
192
+
193
+ # @!visibility private
194
+ # Private extension methods for GamesDice::ComplexDie calculating min and max (which is surprisingly complex)
195
+ module MinMaxHelpers
196
+ private
197
+
198
+ def calc_minmax
199
+ @min_result = probabilities.min
200
+ @max_result = probabilities.max
201
+ return if @probabilities_complete
202
+
203
+ logical_min, logical_max = logical_minmax
204
+ @min_result, @max_result = [@min_result, @max_result, logical_min, logical_max].minmax
205
+ end
206
+
207
+ def minmax_mappings(possible_values)
208
+ possible_values.map do |x|
209
+ map_val, = calc_maps(x)
210
+ map_val
211
+ end.minmax
212
+ end
213
+
214
+ # This isn't 100% accurate, but does cover most "normal" scenarios, and we're only falling back to it when we
215
+ # have to. The inaccuracy is that min_result..max_result may contain 'holes' which have extreme map values that
216
+ # cannot actually occur. In practice it is likely a non-issue unless someone went out of their way to invent a
217
+ # dice schem that broke it.
218
+ def logical_minmax
219
+ return @basic_die.minmax unless @rerolls || @maps
220
+ return minmax_mappings(@basic_die.all_values) unless @rerolls
221
+
222
+ min_result, max_result = logical_rerolls_minmax
223
+ return minmax_mappings(min_result..max_result) if @maps
224
+
225
+ [min_result, max_result]
226
+ end
227
+
228
+ def logical_rerolls_minmax
229
+ min_result = @basic_die.min
230
+ max_result = @basic_die.max
231
+ min_subtract = find_minimum_possible_subtract
232
+ max_add = find_maximum_possible_adds
233
+ min_result = [min_subtract - max_add, min_subtract - max_result].min if min_subtract
234
+ [min_result, max_add + max_result]
235
+ end
236
+
237
+ def find_minimum_possible_subtract
238
+ min_subtract = nil
239
+ @rerolls.select { |r| r.type == :reroll_subtract }.each do |rule|
240
+ min_reroll = @basic_die.all_values.select { |v| rule.applies?(v) }.min
241
+ next unless min_reroll
242
+
243
+ min_subtract = [min_reroll, min_subtract].compact.min
244
+ end
245
+ min_subtract
246
+ end
247
+
248
+ def find_maximum_possible_adds
249
+ total_add = 0
250
+ @rerolls.select { |r| r.type == :reroll_add }.each do |rule|
251
+ max_reroll = @basic_die.all_values.select { |v| rule.applies?(v) }.max
252
+ next unless max_reroll
253
+
254
+ total_add += max_reroll * rule.limit
255
+ end
256
+ total_add
64
257
  end
65
- [result_so_far, rerolls_remaining]
66
258
  end
67
259
  end
68
260
  end
@@ -28,11 +28,14 @@ module GamesDice
28
28
  # @option bunches [Integer] :sides Number of sides on a single die in the bunch, *mandatory*
29
29
  # @option bunches [String] :name Optional name for the bunch
30
30
  # @option bunches [Array<GamesDice::RerollRule,Array>] :rerolls Optional rules that cause the die to roll again
31
- # @option bunches [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result for the die
32
- # @option bunches [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
31
+ # @option bunches [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result
32
+ # for the die
33
+ # @option bunches [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to
34
+ # GamesDice::Die's constructor
33
35
  # @option bunches [Symbol] :keep_mode Optional, either *:keep_best* or *:keep_worst*
34
36
  # @option bunches [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
35
- # @option bunches [Integer] :multiplier Optional, defaults to 1, and typically 1 or -1 to describe whether the Bunch total is to be added or subtracted
37
+ # @option bunches [Integer] :multiplier Optional, defaults to 1, and typically 1 or -1 to describe whether the
38
+ # Bunch total is to be added or subtracted
36
39
  # @return [GamesDice::Dice]
37
40
  def initialize(bunches, offset = 0, name = '')
38
41
  @name = name
@@ -110,14 +113,11 @@ module GamesDice
110
113
 
111
114
  explanations = @bunches.map { |bunch| "#{bunch.label}: #{bunch.explain_result}" }
112
115
 
113
- return @offset.to_s if explanations.count.zero?
116
+ return @offset.to_s if explanations.none?
114
117
 
115
- return simple_explanation(explanations.first) if explanations.count == 1
118
+ return simple_explanation(explanations.first) if explanations.one?
116
119
 
117
- bunch_values = @bunch_multipliers.zip(@bunches).map { |m, b| m * b.result }
118
- bunch_values << @offset if @offset != 0
119
- explanations << array_to_sum(bunch_values)
120
- explanations.join('. ')
120
+ multi_explanations(explanations)
121
121
  end
122
122
 
123
123
  private
@@ -128,8 +128,15 @@ module GamesDice
128
128
  "#{explanation}. #{array_to_sum([@bunches[0].result, @offset])}"
129
129
  end
130
130
 
131
+ def multi_explanations(explanations)
132
+ bunch_values = @bunch_multipliers.zip(@bunches).map { |m, b| m * b.result }
133
+ bunch_values << @offset if @offset != 0
134
+ explanations << array_to_sum(bunch_values)
135
+ explanations.join('. ')
136
+ end
137
+
131
138
  def array_to_sum(array)
132
- (numbers_to_strings(array) + ['=', array.inject(:+)]).join(' ')
139
+ (numbers_to_strings(array) + ['=', array.sum]).join(' ')
133
140
  end
134
141
 
135
142
  def numbers_to_strings(array)
@@ -74,8 +74,8 @@ module GamesDice
74
74
  # Iterates through all possible results on die.
75
75
  # @yieldparam [Integer] result A potential result from the die
76
76
  # @return [GamesDice::Die] this object
77
- def each_value(&block)
78
- (1..@sides).each(&block)
77
+ def each_value(&)
78
+ (1..@sides).each(&)
79
79
  self
80
80
  end
81
81
 
@@ -1,6 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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
+
4
33
  # This class models the output of GamesDice::ComplexDie.
5
34
  #
6
35
  # An object of the class represents the results of a roll of a ComplexDie, including any re-rolls and
@@ -29,6 +58,7 @@ module GamesDice
29
58
  #
30
59
  class DieResult
31
60
  include Comparable
61
+ include ExpressionHelpers
32
62
 
33
63
  # Creates new instance of GamesDice::DieResult. The object can be initialised "empty" or with a first result.
34
64
  # @param [Integer,nil] first_roll_result Value for first roll of the die.
@@ -40,13 +70,9 @@ module GamesDice
40
70
  end
41
71
 
42
72
  if first_roll_result
43
- @rolls = [Integer(first_roll_result)]
44
- @roll_reasons = [first_roll_reason]
45
- @total = @rolls[0]
73
+ init_with_result(first_roll_result, first_roll_reason)
46
74
  else
47
- @rolls = []
48
- @roll_reasons = []
49
- @total = nil
75
+ init_empty
50
76
  end
51
77
  @mapped = false
52
78
  @value = @total
@@ -78,32 +104,13 @@ module GamesDice
78
104
  # @param [Symbol] roll_reason Reason for rolling the die.
79
105
  # @return [Integer] Total so far
80
106
  def add_roll(roll_result, roll_reason = :basic)
81
- unless GamesDice::REROLL_TYPES.key?(roll_reason)
82
- raise ArgumentError, "Unrecognised reason for roll #{roll_reason}"
83
- end
107
+ raise ArgumentError, "Unrecognised roll reason #{roll_reason}" unless GamesDice::REROLL_TYPES.key?(roll_reason)
84
108
 
85
109
  @rolls << Integer(roll_result)
86
110
  @roll_reasons << roll_reason
87
111
  @total = 0 if @rolls.length == 1
88
112
 
89
- case roll_reason
90
- when :basic
91
- @total = roll_result
92
- when :reroll_add
93
- @total += roll_result
94
- when :reroll_subtract
95
- @total -= roll_result
96
- when :reroll_new_die
97
- @total = roll_result
98
- when :reroll_new_keeper
99
- @total = roll_result
100
- when :reroll_replace
101
- @total = roll_result
102
- when :reroll_use_best
103
- @total = [@value, roll_result].max
104
- when :reroll_use_worst
105
- @total = [@value, roll_result].min
106
- end
113
+ apply_roll_to_total(roll_reason, roll_result)
107
114
 
108
115
  @mapped = false
109
116
  @value = @total
@@ -125,17 +132,12 @@ module GamesDice
125
132
  # map description, but does not include the mapped value.
126
133
  # @return [String] Explanation of #value.
127
134
  def explain_value
128
- text = ''
129
- if @rolls.length < 2
130
- text = @total.to_s
131
- else
132
- text = "[#{@rolls[0]}"
133
- text = (1..@rolls.length - 1).inject(text) do |so_far, i|
134
- so_far + GamesDice::REROLL_TYPES[@roll_reasons[i]] + @rolls[i].to_s
135
- end
136
- text += "] #{@total}"
137
- end
138
- text += " #{@map_description}" if @mapped && @map_description && @map_description.length.positive?
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?
139
141
  text
140
142
  end
141
143
 
@@ -143,51 +145,62 @@ module GamesDice
143
145
  # This is mis-named, it doesn't explain the total at all! It is used to generate summaries of keeper dice.
144
146
  def explain_total
145
147
  text = @total.to_s
146
- text += " #{@map_description}" if @mapped && @map_description && @map_description.length.positive?
148
+ text += " #{@map_description}" if @mapped && @map_description&.length&.positive?
147
149
  text
148
150
  end
149
151
 
150
- # @!visibility private
151
- # all coercions simply use #value (i.e. nil or a Integer)
152
- def coerce(thing)
153
- @value.coerce(thing)
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
154
163
  end
155
164
 
156
- # @!visibility private
157
- # addition uses #value
158
- def +(other)
159
- @value + other
160
- end
165
+ private
161
166
 
162
- # @!visibility private
163
- # subtraction uses #value
164
- def -(other)
165
- @value - other
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
166
183
  end
184
+ # rubocop:enable Metrics/MethodLength
167
185
 
168
- # @!visibility private
169
- # multiplication uses #value
170
- def *(other)
171
- @value * other
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]
172
190
  end
173
191
 
174
- # @!visibility private
175
- # comparison <=> uses #value
176
- def <=>(other)
177
- value <=> other
192
+ def init_empty
193
+ @rolls = []
194
+ @roll_reasons = []
195
+ @total = nil
178
196
  end
179
197
 
180
- # This is a deep clone, all attributes are cloned.
181
- # @return [GamesDice::DieResult]
182
- def clone
183
- cloned = GamesDice::DieResult.new
184
- cloned.instance_variable_set('@rolls', @rolls.clone)
185
- cloned.instance_variable_set('@roll_reasons', @roll_reasons.clone)
186
- cloned.instance_variable_set('@total', @total)
187
- cloned.instance_variable_set('@value', @value)
188
- cloned.instance_variable_set('@mapped', @mapped)
189
- cloned.instance_variable_set('@map_description', @map_description)
190
- cloned
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}"
191
204
  end
192
205
  end
193
206
  end
@@ -1,17 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GamesDice
4
- # Probability calculations
4
+ # This class models probability distributions for dice systems.
5
+ #
6
+ # An object of this class represents a single distribution, which might be the result of a complex
7
+ # combination of dice.
8
+ #
9
+ # @example Distribution for a six-sided die
10
+ # probs = GamesDice::Probabilities.for_fair_die( 6 )
11
+ # probs.min # => 1
12
+ # probs.max # => 6
13
+ # probs.expected # => 3.5
14
+ # probs.p_ge( 4 ) # => 0.5
15
+ #
16
+ # @example Adding two distributions
17
+ # pd6 = GamesDice::Probabilities.for_fair_die( 6 )
18
+ # probs = GamesDice::Probabilities.add_distributions( pd6, pd6 )
19
+ # probs.min # => 2
20
+ # probs.max # => 12
21
+ # probs.expected # => 7.0
22
+ # probs.p_ge( 10 ) # => 0.16666666666666669
23
+ #
5
24
  class Probabilities
6
25
  # @!visibility private
7
26
  # Adds support for Marshal, via to_h and from_h methods
8
- def _dump(*_ignored)
9
- Marshal.dump to_h
27
+ def marshal_dump
28
+ to_h
10
29
  end
11
30
 
12
31
  # @!visibility private
13
32
  def self._load(buf)
33
+ # Use of Marshal for general-purpose object serialisation is discouraged. However, this class does support
34
+ # it for backwards-compatibility.
35
+ # rubocop:disable Security/MarshalLoad
14
36
  h = Marshal.load buf
37
+ # rubocop:enable Security/MarshalLoad
15
38
  from_h h
16
39
  end
17
40
  end