games_dice 0.2.3 → 0.2.4
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.
- data/.travis.yml +5 -1
- data/lib/games_dice/bunch.rb +10 -64
- data/lib/games_dice/complex_die.rb +21 -11
- data/lib/games_dice/dice.rb +1 -1
- data/lib/games_dice/die.rb +1 -2
- data/lib/games_dice/probabilities.rb +141 -29
- data/lib/games_dice/version.rb +1 -1
- data/spec/bunch_spec.rb +0 -2
- data/spec/probability_spec.rb +98 -19
- metadata +4 -4
data/.travis.yml
CHANGED
data/lib/games_dice/bunch.rb
CHANGED
|
@@ -158,12 +158,8 @@ class GamesDice::Bunch
|
|
|
158
158
|
return @probabilities if @probabilities
|
|
159
159
|
@probabilities_complete = true
|
|
160
160
|
|
|
161
|
-
# TODO: It is possible to optimise this slightly by combining already-calculated values
|
|
162
|
-
# Adding dice is same as multiplying probability sets for that number of dice
|
|
163
|
-
# Combine(probabililities_3_dice, probabililities_single_die) == Combine(probabililities_2_dice, probabililities_2_dice)
|
|
164
|
-
# It is possible to minimise the total number of multiplications, gaining about 30% efficiency, with careful choices
|
|
165
|
-
single_roll_probs = @single_die.probabilities.to_h
|
|
166
161
|
if @keep_mode && @ndice > @keep_number
|
|
162
|
+
single_roll_probs = @single_die.probabilities.to_h
|
|
167
163
|
preadd_probs = {}
|
|
168
164
|
single_roll_probs.each { |k,v| preadd_probs[k.to_s] = v }
|
|
169
165
|
|
|
@@ -180,15 +176,12 @@ class GamesDice::Bunch
|
|
|
180
176
|
combined_probs[total] ||= 0.0
|
|
181
177
|
combined_probs[total] += v
|
|
182
178
|
end
|
|
179
|
+
@probabilities = GamesDice::Probabilities.from_h( combined_probs )
|
|
183
180
|
else
|
|
184
|
-
|
|
185
|
-
(@ndice-1).times do
|
|
186
|
-
combined_probs = prob_accumulate combined_probs, single_roll_probs
|
|
187
|
-
end
|
|
181
|
+
@probabilities = GamesDice::Probabilities.repeat_distribution( @single_die.probabilities, @ndice )
|
|
188
182
|
end
|
|
189
183
|
|
|
190
|
-
|
|
191
|
-
@probabilities = GamesDice::Probabilities.new( combined_probs )
|
|
184
|
+
return @probabilities
|
|
192
185
|
end
|
|
193
186
|
|
|
194
187
|
# Simulates rolling the bunch of identical dice
|
|
@@ -270,26 +263,10 @@ class GamesDice::Bunch
|
|
|
270
263
|
|
|
271
264
|
private
|
|
272
265
|
|
|
273
|
-
# combines two sets of probabilities where the end result is the first set of keys plus
|
|
274
|
-
# the second set of keys, at the associated probailities of the values
|
|
275
|
-
def prob_accumulate first_probs, second_probs
|
|
276
|
-
accumulator = Hash.new
|
|
277
|
-
|
|
278
|
-
first_probs.each do |v1,p1|
|
|
279
|
-
second_probs.each do |v2,p2|
|
|
280
|
-
v3 = v1 + v2
|
|
281
|
-
p3 = p1 * p2
|
|
282
|
-
accumulator[v3] ||= 0.0
|
|
283
|
-
accumulator[v3] += p3
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
accumulator
|
|
288
|
-
end
|
|
289
|
-
|
|
290
266
|
# combines two sets of probabilities, as above, except tracking unique permutations
|
|
291
267
|
def prob_accumulate_combinations so_far, die_probs, keep_rule = nil
|
|
292
268
|
accumulator = Hash.new
|
|
269
|
+
accumulator.default = 0.0
|
|
293
270
|
|
|
294
271
|
so_far.each do |sig,p1|
|
|
295
272
|
combo = sig.split(';').map { |s| s.to_i }
|
|
@@ -297,33 +274,32 @@ class GamesDice::Bunch
|
|
|
297
274
|
case keep_rule
|
|
298
275
|
when nil then
|
|
299
276
|
die_probs.each do |v2,p2|
|
|
300
|
-
new_sig = (combo + [v2]).sort
|
|
277
|
+
new_sig = (combo + [v2]).sort!.join(';')
|
|
301
278
|
p3 = p1 * p2
|
|
302
|
-
accumulator[new_sig] ||= 0.0
|
|
303
279
|
accumulator[new_sig] += p3
|
|
304
280
|
end
|
|
305
281
|
when :keep_best then
|
|
306
282
|
need_more_than = combo.min
|
|
283
|
+
len = combo.size
|
|
307
284
|
die_probs.each do |v2,p2|
|
|
308
285
|
if v2 > need_more_than
|
|
309
|
-
new_sig = (combo + [v2]).sort[1
|
|
286
|
+
new_sig = (combo + [v2]).sort![1,len].join(';')
|
|
310
287
|
else
|
|
311
288
|
new_sig = sig
|
|
312
289
|
end
|
|
313
290
|
p3 = p1 * p2
|
|
314
|
-
accumulator[new_sig] ||= 0.0
|
|
315
291
|
accumulator[new_sig] += p3
|
|
316
292
|
end
|
|
317
293
|
when :keep_worst then
|
|
318
294
|
need_less_than = combo.max
|
|
295
|
+
len = combo.size
|
|
319
296
|
die_probs.each do |v2,p2|
|
|
320
297
|
if v2 < need_less_than
|
|
321
|
-
new_sig = (combo + [v2]).sort[0
|
|
298
|
+
new_sig = (combo + [v2]).sort![0,len].join(';')
|
|
322
299
|
else
|
|
323
300
|
new_sig = sig
|
|
324
301
|
end
|
|
325
302
|
p3 = p1 * p2
|
|
326
|
-
accumulator[new_sig] ||= 0.0
|
|
327
303
|
accumulator[new_sig] += p3
|
|
328
304
|
end
|
|
329
305
|
end
|
|
@@ -332,34 +308,4 @@ class GamesDice::Bunch
|
|
|
332
308
|
accumulator
|
|
333
309
|
end
|
|
334
310
|
|
|
335
|
-
# Generates all sets of [throw_away,may_keep_exactly,keep_preferentially,combinations] that meet
|
|
336
|
-
# criteria for correct total number of dice and keep dice. These then need to be assessed for every
|
|
337
|
-
# die value by the caller to get a full set of probabilities
|
|
338
|
-
def generate_item_counts total_dice, keep_dice
|
|
339
|
-
# Constraints are:
|
|
340
|
-
# may_keep_exactly must be at least 1, and at most is all the dice
|
|
341
|
-
# keep_preferentially plus may_keep_exactly must be >= keep_dice, but keep_preferentially < keep dice
|
|
342
|
-
# sum of all three always == total_dice
|
|
343
|
-
item_counts = []
|
|
344
|
-
(1..total_dice).each do |may_keep_exactly|
|
|
345
|
-
min_kp = [keep_dice - may_keep_exactly, 0].max
|
|
346
|
-
max_kp = [keep_dice - 1, total_dice - may_keep_exactly].min
|
|
347
|
-
(min_kp..max_kp).each do |keep_preferentially|
|
|
348
|
-
counts = [ total_dice - may_keep_exactly - keep_preferentially, may_keep_exactly, keep_preferentially ]
|
|
349
|
-
counts << combinations(counts)
|
|
350
|
-
item_counts << counts
|
|
351
|
-
end
|
|
352
|
-
end
|
|
353
|
-
item_counts
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
# How many unique ways can a set of items, some of which are identical, be arranged?
|
|
357
|
-
def combinations item_counts
|
|
358
|
-
item_counts = item_counts.map { |i| Integer(i) }.select { |i| i > 0 }
|
|
359
|
-
total_items = item_counts.inject(:+)
|
|
360
|
-
numerator = 1.upto(total_items).inject(:*)
|
|
361
|
-
denominator = item_counts.map { |i| 1.upto(i).inject(:*) }.inject(:*)
|
|
362
|
-
numerator / denominator
|
|
363
|
-
end
|
|
364
|
-
|
|
365
311
|
end # class Bunch
|
|
@@ -75,8 +75,10 @@ class GamesDice::ComplexDie
|
|
|
75
75
|
@result.explain_value
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# The minimum possible result from a call to #roll. This is not always the same as the theoretical
|
|
79
|
+
# minimum, due to limits on the maximum number of rerolls.
|
|
78
80
|
# @!attribute [r] min
|
|
79
|
-
# @return [Integer]
|
|
81
|
+
# @return [Integer]
|
|
80
82
|
def min
|
|
81
83
|
return @min_result if @min_result
|
|
82
84
|
@min_result, @max_result = [probabilities.min, probabilities.max]
|
|
@@ -115,19 +117,17 @@ class GamesDice::ComplexDie
|
|
|
115
117
|
elsif @rerolls
|
|
116
118
|
prob_hash = recursive_probabilities
|
|
117
119
|
elsif @maps
|
|
118
|
-
probs = @basic_die.probabilities.to_h
|
|
119
120
|
prob_hash = {}
|
|
120
|
-
|
|
121
|
+
@basic_die.probabilities.each do |v,p|
|
|
121
122
|
m, n = calc_maps(v)
|
|
122
123
|
prob_hash[m] ||= 0.0
|
|
123
124
|
prob_hash[m] += p
|
|
124
125
|
end
|
|
125
126
|
else
|
|
126
|
-
|
|
127
|
+
@probabilities = @basic_die.probabilities
|
|
128
|
+
return @probabilities
|
|
127
129
|
end
|
|
128
|
-
@
|
|
129
|
-
@prob_le = {}
|
|
130
|
-
@probabilities = GamesDice::Probabilities.new( prob_hash )
|
|
130
|
+
@probabilities = GamesDice::Probabilities.from_h( prob_hash )
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
# Simulates rolling the die
|
|
@@ -250,9 +250,7 @@ class GamesDice::ComplexDie
|
|
|
250
250
|
|
|
251
251
|
(1..@basic_die.sides).each do |v|
|
|
252
252
|
# calculate value, recurse if there is a reroll
|
|
253
|
-
result_so_far = prior_result
|
|
254
|
-
result_so_far.add_roll(v,roll_reason) if prior_result
|
|
255
|
-
rerolls_remaining = rerolls_left ? rerolls_left.clone : @rerolls.map { |rule| rule.limit }
|
|
253
|
+
result_so_far, rerolls_remaining = calc_result_so_far(prior_result, rerolls_left, v, roll_reason )
|
|
256
254
|
|
|
257
255
|
# Find which rule, if any, is being triggered
|
|
258
256
|
rule_idx = @rerolls.zip(rerolls_remaining).find_index do |rule,remaining|
|
|
@@ -279,7 +277,19 @@ class GamesDice::ComplexDie
|
|
|
279
277
|
end
|
|
280
278
|
|
|
281
279
|
end
|
|
282
|
-
probabilities
|
|
280
|
+
probabilities
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def calc_result_so_far prior_result, rerolls_left, v, roll_reason
|
|
284
|
+
if prior_result
|
|
285
|
+
result_so_far = prior_result.clone
|
|
286
|
+
result_so_far.add_roll(v,roll_reason)
|
|
287
|
+
rerolls_remaining = rerolls_left.clone
|
|
288
|
+
else
|
|
289
|
+
result_so_far = GamesDice::DieResult.new(v,roll_reason)
|
|
290
|
+
rerolls_remaining = @rerolls.map { |rule| rule.limit }
|
|
291
|
+
end
|
|
292
|
+
[result_so_far, rerolls_remaining]
|
|
283
293
|
end
|
|
284
294
|
|
|
285
295
|
end # class ComplexDie
|
data/lib/games_dice/dice.rb
CHANGED
|
@@ -102,7 +102,7 @@ class GamesDice::Dice
|
|
|
102
102
|
# @return [GamesDice::Probabilities] Probability distribution of dice.
|
|
103
103
|
def probabilities
|
|
104
104
|
return @probabilities if @probabilities
|
|
105
|
-
probs = @bunch_multipliers.zip(@bunches).inject( GamesDice::Probabilities.new(
|
|
105
|
+
probs = @bunch_multipliers.zip(@bunches).inject( GamesDice::Probabilities.new( [1.0], @offset ) ) do |probs, mb|
|
|
106
106
|
m,b = mb
|
|
107
107
|
GamesDice::Probabilities.add_distributions_mult( 1, probs, m, b.probabilities )
|
|
108
108
|
end
|
data/lib/games_dice/die.rb
CHANGED
|
@@ -55,8 +55,7 @@ class GamesDice::Die
|
|
|
55
55
|
# Calculates probability distribution for this die.
|
|
56
56
|
# @return [GamesDice::Probabilities] probability distribution of the die
|
|
57
57
|
def probabilities
|
|
58
|
-
|
|
59
|
-
@probabilities = GamesDice::Probabilities.for_fair_die( @sides )
|
|
58
|
+
@probabilities ||= GamesDice::Probabilities.for_fair_die( @sides )
|
|
60
59
|
end
|
|
61
60
|
|
|
62
61
|
# Simulates rolling the die
|
|
@@ -21,52 +21,66 @@
|
|
|
21
21
|
class GamesDice::Probabilities
|
|
22
22
|
|
|
23
23
|
# Creates new instance of GamesDice::Probabilities.
|
|
24
|
-
# @param [
|
|
25
|
-
#
|
|
24
|
+
# @param [Array<Float>] probs Each entry in the array is the probability of getting a result
|
|
25
|
+
# @param [Integer] offset The result associated with index of 0 in the array
|
|
26
26
|
# @return [GamesDice::Probabilities]
|
|
27
|
-
def initialize(
|
|
27
|
+
def initialize( probs = [1.0], offset = 0 )
|
|
28
28
|
# This should *probably* be validated in future, but that would impact performance
|
|
29
|
-
@
|
|
29
|
+
@probs = probs
|
|
30
|
+
@offset = offset
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# @!visibility private
|
|
33
|
-
# the
|
|
34
|
-
|
|
34
|
+
# the Array, Offset representation of probabilities.
|
|
35
|
+
def to_ao
|
|
36
|
+
[ @probs, @offset ]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Iterates through value, probability pairs
|
|
40
|
+
# @yieldparam [Integer] result A result that may be possible in the dice scheme
|
|
41
|
+
# @yieldparam [Float] probability Probability of result, in range 0.0..1.0
|
|
42
|
+
# @return [GamesDice::Probabilities] this object
|
|
43
|
+
def each
|
|
44
|
+
@probs.each_with_index { |p,i| yield( i+@offset, p ) }
|
|
45
|
+
return self
|
|
46
|
+
end
|
|
35
47
|
|
|
36
48
|
# A hash representation of the distribution. Each key is an integer result,
|
|
37
49
|
# and the matching value is probability of getting that result. A new hash is generated on each
|
|
38
50
|
# call to this method.
|
|
39
51
|
# @return [Hash]
|
|
40
52
|
def to_h
|
|
41
|
-
@
|
|
53
|
+
GamesDice::Probabilities.prob_ao_to_h( @probs, @offset )
|
|
42
54
|
end
|
|
43
55
|
|
|
44
56
|
# @!attribute [r] min
|
|
45
57
|
# Minimum result in the distribution
|
|
46
58
|
# @return [Integer]
|
|
47
59
|
def min
|
|
48
|
-
|
|
60
|
+
@offset
|
|
49
61
|
end
|
|
50
62
|
|
|
51
63
|
# @!attribute [r] max
|
|
52
64
|
# Maximum result in the distribution
|
|
53
65
|
# @return [Integer]
|
|
54
66
|
def max
|
|
55
|
-
|
|
67
|
+
@offset + @probs.count() - 1
|
|
56
68
|
end
|
|
57
69
|
|
|
58
70
|
# @!attribute [r] expected
|
|
59
71
|
# Expected value of distribution.
|
|
60
72
|
# @return [Float]
|
|
61
73
|
def expected
|
|
62
|
-
@expected ||=
|
|
74
|
+
@expected ||= calc_expected
|
|
63
75
|
end
|
|
64
76
|
|
|
65
77
|
# Probability of result equalling specific target
|
|
66
78
|
# @param [Integer] target
|
|
67
79
|
# @return [Float] in range (0.0..1.0)
|
|
68
80
|
def p_eql target
|
|
69
|
-
|
|
81
|
+
i = Integer(target) - @offset
|
|
82
|
+
return 0.0 if i < 0 || i >= @probs.count
|
|
83
|
+
@probs[ i ]
|
|
70
84
|
end
|
|
71
85
|
|
|
72
86
|
# Probability of result being greater than specific target
|
|
@@ -86,7 +100,7 @@ class GamesDice::Probabilities
|
|
|
86
100
|
|
|
87
101
|
return 1.0 if target <= min
|
|
88
102
|
return 0.0 if target > max
|
|
89
|
-
@prob_ge[target] = @
|
|
103
|
+
@prob_ge[target] = @probs[target-@offset,@probs.count-1].inject(0.0) {|so_far,p| so_far + p }
|
|
90
104
|
end
|
|
91
105
|
|
|
92
106
|
# Probability of result being equal to or less than specific target
|
|
@@ -99,7 +113,7 @@ class GamesDice::Probabilities
|
|
|
99
113
|
|
|
100
114
|
return 1.0 if target >= max
|
|
101
115
|
return 0.0 if target < min
|
|
102
|
-
@prob_le[target] = @
|
|
116
|
+
@prob_le[target] = @probs[0,1+target-@offset].inject(0.0) {|so_far,p| so_far + p }
|
|
103
117
|
end
|
|
104
118
|
|
|
105
119
|
# Probability of result being less than specific target
|
|
@@ -109,16 +123,50 @@ class GamesDice::Probabilities
|
|
|
109
123
|
p_le( Integer(target) - 1 )
|
|
110
124
|
end
|
|
111
125
|
|
|
126
|
+
# Probability distribution derived from this one, where we know (or are only interested in
|
|
127
|
+
# situations where) the result is greater than or equal to target.
|
|
128
|
+
# @param [Integer] target
|
|
129
|
+
# @return [GamesDice::Probabilities] new distribution.
|
|
130
|
+
def given_ge target
|
|
131
|
+
target = Integer(target)
|
|
132
|
+
target = min if min > target
|
|
133
|
+
p = p_ge(target)
|
|
134
|
+
raise "There is no valid distribution given a result >= #{target}" unless p > 0.0
|
|
135
|
+
mult = 1.0/p
|
|
136
|
+
new_probs = @probs[target-@offset,@probs.count-1].map { |x| x * mult }
|
|
137
|
+
GamesDice::Probabilities.new( new_probs, target )
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Probability distribution derived from this one, where we know (or are only interested in
|
|
141
|
+
# situations where) the result is less than or equal to target.
|
|
142
|
+
# @param [Integer] target
|
|
143
|
+
# @return [GamesDice::Probabilities] new distribution.
|
|
144
|
+
def given_le target
|
|
145
|
+
target = Integer(target)
|
|
146
|
+
target = max if max < target
|
|
147
|
+
p = p_le(target)
|
|
148
|
+
raise "There is no valid distribution given a result <= #{target}" unless p > 0.0
|
|
149
|
+
mult = 1.0/p
|
|
150
|
+
new_probs = @probs[0..target-@offset].map { |x| x * mult }
|
|
151
|
+
GamesDice::Probabilities.new( new_probs, @offset )
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Creates new instance of GamesDice::Probabilities.
|
|
155
|
+
# @param [Hash] prob_hash A hash representation of the distribution, each key is an integer result,
|
|
156
|
+
# and the matching value is probability of getting that result
|
|
157
|
+
# @return [GamesDice::Probabilities]
|
|
158
|
+
def self.from_h prob_hash
|
|
159
|
+
probs, offset = prob_h_to_ao( prob_hash )
|
|
160
|
+
GamesDice::Probabilities.new( probs, offset )
|
|
161
|
+
end
|
|
162
|
+
|
|
112
163
|
# Distribution for a die with equal chance of rolling 1..N
|
|
113
164
|
# @param [Integer] sides Number of sides on die
|
|
114
165
|
# @return [GamesDice::Probabilities]
|
|
115
166
|
def self.for_fair_die sides
|
|
116
167
|
sides = Integer(sides)
|
|
117
168
|
raise ArgumentError, "sides must be at least 1" unless sides > 0
|
|
118
|
-
|
|
119
|
-
p = 1.0/sides
|
|
120
|
-
(1..sides).each { |x| h[x] = p }
|
|
121
|
-
GamesDice::Probabilities.new( h )
|
|
169
|
+
GamesDice::Probabilities.new( Array.new( sides, 1.0/sides ), 1 )
|
|
122
170
|
end
|
|
123
171
|
|
|
124
172
|
# Combines two distributions to create a third, that represents the distribution created when adding
|
|
@@ -127,15 +175,20 @@ class GamesDice::Probabilities
|
|
|
127
175
|
# @param [GamesDice::Probabilities] pd_b Second distribution
|
|
128
176
|
# @return [GamesDice::Probabilities]
|
|
129
177
|
def self.add_distributions pd_a, pd_b
|
|
130
|
-
|
|
131
|
-
pd_a.
|
|
132
|
-
|
|
133
|
-
|
|
178
|
+
combined_min = pd_a.min + pd_b.min
|
|
179
|
+
combined_max = pd_a.max + pd_b.max
|
|
180
|
+
new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
|
|
181
|
+
probs_a, offset_a = pd_a.to_ao
|
|
182
|
+
probs_b, offset_b = pd_b.to_ao
|
|
183
|
+
|
|
184
|
+
probs_a.each_with_index do |pa,i|
|
|
185
|
+
probs_b.each_with_index do |pb,j|
|
|
186
|
+
k = i + j
|
|
134
187
|
pc = pa * pb
|
|
135
|
-
|
|
188
|
+
new_probs[ k ] += pc
|
|
136
189
|
end
|
|
137
190
|
end
|
|
138
|
-
GamesDice::Probabilities.new(
|
|
191
|
+
GamesDice::Probabilities.new( new_probs, combined_min )
|
|
139
192
|
end
|
|
140
193
|
|
|
141
194
|
# Combines two distributions with multipliers to create a third, that represents the distribution
|
|
@@ -146,15 +199,74 @@ class GamesDice::Probabilities
|
|
|
146
199
|
# @param [GamesDice::Probabilities] pd_b Second distribution
|
|
147
200
|
# @return [GamesDice::Probabilities]
|
|
148
201
|
def self.add_distributions_mult m_a, pd_a, m_b, pd_b
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
pd_b.
|
|
152
|
-
|
|
202
|
+
combined_min, combined_max = [
|
|
203
|
+
m_a * pd_a.min + m_b * pd_b.min, m_a * pd_a.max + m_b * pd_b.min,
|
|
204
|
+
m_a * pd_a.min + m_b * pd_b.max, m_a * pd_a.max + m_b * pd_b.max,
|
|
205
|
+
].minmax
|
|
206
|
+
|
|
207
|
+
new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
|
|
208
|
+
probs_a, offset_a = pd_a.to_ao
|
|
209
|
+
probs_b, offset_b = pd_b.to_ao
|
|
210
|
+
|
|
211
|
+
probs_a.each_with_index do |pa,i|
|
|
212
|
+
probs_b.each_with_index do |pb,j|
|
|
213
|
+
k = m_a * (i + offset_a) + m_b * (j + offset_b) - combined_min
|
|
153
214
|
pc = pa * pb
|
|
154
|
-
|
|
215
|
+
new_probs[ k ] += pc
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
GamesDice::Probabilities.new( new_probs, combined_min )
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Adds a distribution to itself repeatedly, to simulate a number of dice
|
|
223
|
+
# results being summed.
|
|
224
|
+
# @param [GamesDice::Probabilities] pd Distribution to repeat
|
|
225
|
+
# @param [Integer] n Number of repetitions, must be at least 1
|
|
226
|
+
# @return [GamesDice::Probabilities]
|
|
227
|
+
def self.repeat_distribution pd, n
|
|
228
|
+
n = Integer( n )
|
|
229
|
+
raise "Cannot combine probabilities less than once" if n < 1
|
|
230
|
+
revbin = n.to_s(2).reverse.each_char.to_a.map { |c| c == '1' }
|
|
231
|
+
pd_power = pd
|
|
232
|
+
pd_result = nil
|
|
233
|
+
max_power = revbin.count - 1
|
|
234
|
+
|
|
235
|
+
revbin.each_with_index do |use_power, i|
|
|
236
|
+
if use_power
|
|
237
|
+
if pd_result
|
|
238
|
+
pd_result = add_distributions( pd_result, pd_power )
|
|
239
|
+
else
|
|
240
|
+
pd_result = pd_power
|
|
241
|
+
end
|
|
155
242
|
end
|
|
243
|
+
pd_power = add_distributions( pd_power, pd_power ) unless i == max_power
|
|
156
244
|
end
|
|
157
|
-
|
|
245
|
+
pd_result
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private
|
|
249
|
+
|
|
250
|
+
# Convert hash to array,offset notation
|
|
251
|
+
def self.prob_h_to_ao h
|
|
252
|
+
rmin,rmax = h.keys.minmax
|
|
253
|
+
o = rmin
|
|
254
|
+
a = Array.new( 1 + rmax - rmin, 0.0 )
|
|
255
|
+
h.each { |k,v| a[k-rmin] = v }
|
|
256
|
+
[a,o]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Convert array,offset notation to hash
|
|
260
|
+
def self.prob_ao_to_h a, o
|
|
261
|
+
h = Hash.new
|
|
262
|
+
a.each_with_index { |v,i| h[i+o] = v if v > 0.0 }
|
|
263
|
+
h
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def calc_expected
|
|
267
|
+
total = 0.0
|
|
268
|
+
@probs.each_with_index { |v,i| total += (i+@offset)*v }
|
|
269
|
+
total
|
|
158
270
|
end
|
|
159
271
|
|
|
160
272
|
end # class GamesDice::Probabilities
|
data/lib/games_dice/version.rb
CHANGED
data/spec/bunch_spec.rb
CHANGED
|
@@ -260,12 +260,10 @@ describe GamesDice::Bunch do
|
|
|
260
260
|
end
|
|
261
261
|
|
|
262
262
|
it "should have a mean value of roughly 18.986" do
|
|
263
|
-
pending "Too slow"
|
|
264
263
|
bunch.probabilities.expected.should be_within(1e-9).of 18.9859925804
|
|
265
264
|
end
|
|
266
265
|
|
|
267
266
|
it "should calculate probabilities correctly" do
|
|
268
|
-
pending "Too slow"
|
|
269
267
|
prob_hash = bunch.probabilities.to_h
|
|
270
268
|
prob_hash[2].should be_within(1e-10).of 0.00001
|
|
271
269
|
prob_hash[3].should be_within(1e-10).of 0.00005
|
data/spec/probability_spec.rb
CHANGED
|
@@ -2,12 +2,10 @@ require 'games_dice'
|
|
|
2
2
|
require 'helpers'
|
|
3
3
|
|
|
4
4
|
describe GamesDice::Probabilities do
|
|
5
|
-
|
|
6
5
|
describe "class methods" do
|
|
7
|
-
|
|
8
6
|
describe "#new" do
|
|
9
|
-
it "should create a new distribution from
|
|
10
|
-
p = GamesDice::Probabilities.new(
|
|
7
|
+
it "should create a new distribution from an array and offset" do
|
|
8
|
+
p = GamesDice::Probabilities.new( [1.0], 1 )
|
|
11
9
|
p.is_a?( GamesDice::Probabilities ).should be_true
|
|
12
10
|
p.to_h.should be_valid_distribution
|
|
13
11
|
end
|
|
@@ -31,8 +29,8 @@ describe GamesDice::Probabilities do
|
|
|
31
29
|
|
|
32
30
|
describe "#add_distributions" do
|
|
33
31
|
it "should combine two distributions to create a third one" do
|
|
34
|
-
d4a = GamesDice::Probabilities.new(
|
|
35
|
-
d4b = GamesDice::Probabilities.new(
|
|
32
|
+
d4a = GamesDice::Probabilities.new( [ 1.0/4, 1.0/4, 1.0/4, 1.0/4 ], 1 )
|
|
33
|
+
d4b = GamesDice::Probabilities.new( [ 1.0/10, 2.0/10, 3.0/10, 4.0/10], 1 )
|
|
36
34
|
p = GamesDice::Probabilities.add_distributions( d4a, d4b )
|
|
37
35
|
p.to_h.should be_valid_distribution
|
|
38
36
|
end
|
|
@@ -40,18 +38,79 @@ describe GamesDice::Probabilities do
|
|
|
40
38
|
it "should calculate a classic 2d6 distribution accurately" do
|
|
41
39
|
d6 = GamesDice::Probabilities.for_fair_die( 6 )
|
|
42
40
|
p = GamesDice::Probabilities.add_distributions( d6, d6 )
|
|
41
|
+
h = p.to_h
|
|
42
|
+
h.should be_valid_distribution
|
|
43
|
+
h[2].should be_within(1e-9).of 1.0/36
|
|
44
|
+
h[3].should be_within(1e-9).of 2.0/36
|
|
45
|
+
h[4].should be_within(1e-9).of 3.0/36
|
|
46
|
+
h[5].should be_within(1e-9).of 4.0/36
|
|
47
|
+
h[6].should be_within(1e-9).of 5.0/36
|
|
48
|
+
h[7].should be_within(1e-9).of 6.0/36
|
|
49
|
+
h[8].should be_within(1e-9).of 5.0/36
|
|
50
|
+
h[9].should be_within(1e-9).of 4.0/36
|
|
51
|
+
h[10].should be_within(1e-9).of 3.0/36
|
|
52
|
+
h[11].should be_within(1e-9).of 2.0/36
|
|
53
|
+
h[12].should be_within(1e-9).of 1.0/36
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "#repeat_distribution" do
|
|
58
|
+
it "should output a valid distribution if params are valid" do
|
|
59
|
+
d4a = GamesDice::Probabilities.new( [ 1.0/4, 1.0/4, 1.0/4, 1.0/4 ], 1 )
|
|
60
|
+
d4b = GamesDice::Probabilities.new( [ 1.0/10, 2.0/10, 3.0/10, 4.0/10], 1 )
|
|
61
|
+
p = GamesDice::Probabilities.repeat_distribution( d4a, 7 )
|
|
62
|
+
p.to_h.should be_valid_distribution
|
|
63
|
+
p = GamesDice::Probabilities.repeat_distribution( d4b, 12 )
|
|
43
64
|
p.to_h.should be_valid_distribution
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
p.
|
|
49
|
-
p.to_h
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "should calculate a classic 3d6 distribution accurately" do
|
|
68
|
+
d6 = GamesDice::Probabilities.for_fair_die( 6 )
|
|
69
|
+
p = GamesDice::Probabilities.repeat_distribution( d6, 3 )
|
|
70
|
+
h = p.to_h
|
|
71
|
+
h.should be_valid_distribution
|
|
72
|
+
h[3].should be_within(1e-9).of 1.0/216
|
|
73
|
+
h[4].should be_within(1e-9).of 3.0/216
|
|
74
|
+
h[5].should be_within(1e-9).of 6.0/216
|
|
75
|
+
h[6].should be_within(1e-9).of 10.0/216
|
|
76
|
+
h[7].should be_within(1e-9).of 15.0/216
|
|
77
|
+
h[8].should be_within(1e-9).of 21.0/216
|
|
78
|
+
h[9].should be_within(1e-9).of 25.0/216
|
|
79
|
+
h[10].should be_within(1e-9).of 27.0/216
|
|
80
|
+
h[11].should be_within(1e-9).of 27.0/216
|
|
81
|
+
h[12].should be_within(1e-9).of 25.0/216
|
|
82
|
+
h[13].should be_within(1e-9).of 21.0/216
|
|
83
|
+
h[14].should be_within(1e-9).of 15.0/216
|
|
84
|
+
h[15].should be_within(1e-9).of 10.0/216
|
|
85
|
+
h[16].should be_within(1e-9).of 6.0/216
|
|
86
|
+
h[17].should be_within(1e-9).of 3.0/216
|
|
87
|
+
h[18].should be_within(1e-9).of 1.0/216
|
|
88
|
+
end
|
|
89
|
+
end # describe "#repeat_distribution"
|
|
90
|
+
|
|
91
|
+
describe "#add_distributions_mult" do
|
|
92
|
+
it "should combine two multiplied distributions to create a third one" do
|
|
93
|
+
d4a = GamesDice::Probabilities.new( [ 1.0/4, 1.0/4, 1.0/4, 1.0/4 ], 1 )
|
|
94
|
+
d4b = GamesDice::Probabilities.new( [ 1.0/10, 2.0/10, 3.0/10, 4.0/10], 1 )
|
|
95
|
+
p = GamesDice::Probabilities.add_distributions_mult( 2, d4a, -1, d4b )
|
|
96
|
+
p.to_h.should be_valid_distribution
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "should calculate a distribution for '1d6 - 1d4' accurately" do
|
|
100
|
+
d6 = GamesDice::Probabilities.for_fair_die( 6 )
|
|
101
|
+
d4 = GamesDice::Probabilities.for_fair_die( 4 )
|
|
102
|
+
p = GamesDice::Probabilities.add_distributions_mult( 1, d6, -1, d4 )
|
|
103
|
+
h = p.to_h
|
|
104
|
+
h.should be_valid_distribution
|
|
105
|
+
h[-3].should be_within(1e-9).of 1.0/24
|
|
106
|
+
h[-2].should be_within(1e-9).of 2.0/24
|
|
107
|
+
h[-1].should be_within(1e-9).of 3.0/24
|
|
108
|
+
h[0].should be_within(1e-9).of 4.0/24
|
|
109
|
+
h[1].should be_within(1e-9).of 4.0/24
|
|
110
|
+
h[2].should be_within(1e-9).of 4.0/24
|
|
111
|
+
h[3].should be_within(1e-9).of 3.0/24
|
|
112
|
+
h[4].should be_within(1e-9).of 2.0/24
|
|
113
|
+
h[5].should be_within(1e-9).of 1.0/24
|
|
55
114
|
end
|
|
56
115
|
end
|
|
57
116
|
|
|
@@ -62,7 +121,7 @@ describe GamesDice::Probabilities do
|
|
|
62
121
|
let(:p4) { GamesDice::Probabilities.for_fair_die( 4 ) }
|
|
63
122
|
let(:p6) { GamesDice::Probabilities.for_fair_die( 6 ) }
|
|
64
123
|
let(:p10) { GamesDice::Probabilities.for_fair_die( 10 ) }
|
|
65
|
-
let(:pa) { GamesDice::Probabilities.new(
|
|
124
|
+
let(:pa) { GamesDice::Probabilities.new( [ 0.4, 0.2, 0.4 ], -1 ) }
|
|
66
125
|
|
|
67
126
|
describe "#p_eql" do
|
|
68
127
|
it "should return probability of getting a number inside the range" do
|
|
@@ -222,6 +281,26 @@ describe GamesDice::Probabilities do
|
|
|
222
281
|
end
|
|
223
282
|
end
|
|
224
283
|
|
|
225
|
-
|
|
284
|
+
describe "#given_ge" do
|
|
285
|
+
it "should return a new distribution with probabilities calculated assuming value is >= target" do
|
|
286
|
+
pd = p2.given_ge(2)
|
|
287
|
+
pd.to_h.should == { 2 => 1.0 }
|
|
288
|
+
pd = p10.given_ge(4)
|
|
289
|
+
pd.to_h.should be_valid_distribution
|
|
290
|
+
pd.p_eql( 3 ).should == 0.0
|
|
291
|
+
pd.p_eql( 10 ).should be_within(1.0e-9).of 0.1/0.7
|
|
292
|
+
end
|
|
293
|
+
end
|
|
226
294
|
|
|
295
|
+
describe "#given_le" do
|
|
296
|
+
it "should return a new distribution with probabilities calculated assuming value is <= target" do
|
|
297
|
+
pd = p2.given_le(2)
|
|
298
|
+
pd.to_h.should == { 1 => 0.5, 2 => 0.5 }
|
|
299
|
+
pd = p10.given_le(4)
|
|
300
|
+
pd.to_h.should be_valid_distribution
|
|
301
|
+
pd.p_eql( 3 ).should be_within(1.0e-9).of 0.1/0.4
|
|
302
|
+
pd.p_eql( 10 ).should == 0.0
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end # describe "instance methods"
|
|
227
306
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: games_dice
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2013-06-
|
|
12
|
+
date: 2013-06-18 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rspec
|
|
@@ -144,7 +144,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
144
144
|
version: '0'
|
|
145
145
|
segments:
|
|
146
146
|
- 0
|
|
147
|
-
hash:
|
|
147
|
+
hash: 1993011777003280822
|
|
148
148
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
149
|
none: false
|
|
150
150
|
requirements:
|
|
@@ -153,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
153
153
|
version: '0'
|
|
154
154
|
segments:
|
|
155
155
|
- 0
|
|
156
|
-
hash:
|
|
156
|
+
hash: 1993011777003280822
|
|
157
157
|
requirements: []
|
|
158
158
|
rubyforge_project:
|
|
159
159
|
rubygems_version: 1.8.24
|