games_dice 0.2.3 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|