games_dice 0.0.3 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/games_dice.gemspec +4 -2
- data/lib/games_dice.rb +3 -0
- data/lib/games_dice/bunch.rb +275 -320
- data/lib/games_dice/complex_die.rb +226 -269
- data/lib/games_dice/constants.rb +16 -0
- data/lib/games_dice/dice.rb +43 -0
- data/lib/games_dice/die.rb +58 -92
- data/lib/games_dice/die_result.rb +3 -15
- data/lib/games_dice/map_rule.rb +41 -43
- data/lib/games_dice/probabilities.rb +97 -0
- data/lib/games_dice/reroll_rule.rb +41 -58
- data/lib/games_dice/version.rb +1 -1
- data/spec/bunch_spec.rb +196 -188
- data/spec/complex_die_spec.rb +77 -68
- data/spec/dice_spec.rb +34 -0
- data/spec/die_spec.rb +25 -29
- data/spec/probability_spec.rb +265 -0
- metadata +31 -8
data/games_dice.gemspec
CHANGED
@@ -13,8 +13,10 @@ Gem::Specification.new do |gem|
|
|
13
13
|
gem.summary = %q{Simulates and explains dice rolls from a variety of game systems.}
|
14
14
|
gem.homepage = "https://github.com/neilslater/games_dice"
|
15
15
|
|
16
|
-
gem.add_development_dependency "rspec"
|
17
|
-
gem.add_development_dependency "rake"
|
16
|
+
gem.add_development_dependency "rspec", ">= 2.13.0"
|
17
|
+
gem.add_development_dependency "rake", ">= 1.9.1"
|
18
|
+
|
19
|
+
gem.add_dependency "parslet", "~> 1.5.0"
|
18
20
|
|
19
21
|
gem.files = `git ls-files`.split($/)
|
20
22
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
data/lib/games_dice.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
require "games_dice/version"
|
2
|
+
require "games_dice/constants"
|
3
|
+
require "games_dice/probabilities"
|
2
4
|
require "games_dice/die"
|
3
5
|
require "games_dice/die_result"
|
4
6
|
require "games_dice/reroll_rule"
|
5
7
|
require "games_dice/map_rule"
|
6
8
|
require "games_dice/complex_die"
|
7
9
|
require "games_dice/bunch"
|
10
|
+
require "games_dice/dice"
|
8
11
|
|
9
12
|
module GamesDice
|
10
13
|
# TODO: Factory methods for various dice schemes
|
data/lib/games_dice/bunch.rb
CHANGED
@@ -1,367 +1,322 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
prng = attributes[:prng]
|
31
|
-
raise ":prng does not support the rand() method" if ! prng.respond_to?(:rand)
|
32
|
-
end
|
33
|
-
|
34
|
-
needs_complex_die = false
|
35
|
-
|
36
|
-
if attributes[:rerolls]
|
37
|
-
needs_complex_die = true
|
38
|
-
options[:rerolls] = attributes[:rerolls].clone
|
39
|
-
end
|
40
|
-
|
41
|
-
if attributes[:maps]
|
42
|
-
needs_complex_die = true
|
43
|
-
options[:maps] = attributes[:maps].clone
|
44
|
-
end
|
45
|
-
|
46
|
-
if needs_complex_die
|
47
|
-
options[:prng] = prng
|
48
|
-
@single_die = GamesDice::ComplexDie.new( @sides, options )
|
49
|
-
else
|
50
|
-
@single_die = GamesDice::Die.new( @sides, prng )
|
51
|
-
end
|
52
|
-
|
53
|
-
case attributes[:keep_mode]
|
54
|
-
when nil then
|
55
|
-
@keep_mode = nil
|
56
|
-
when :keep_best then
|
57
|
-
@keep_mode = :keep_best
|
58
|
-
@keep_number = Integer(attributes[:keep_number] || 1)
|
59
|
-
when :keep_worst then
|
60
|
-
@keep_mode = :keep_worst
|
61
|
-
@keep_number = Integer(attributes[:keep_number] || 1)
|
62
|
-
else
|
63
|
-
raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{attributes[:keep_mode].inspect}"
|
64
|
-
end
|
1
|
+
# models a set of identical dice, that can be "rolled" and combined into a simple integer result. The
|
2
|
+
# dice are identical in number of sides, and any re-roll or mapping rules that apply to them
|
3
|
+
class GamesDice::Bunch
|
4
|
+
# attributes is a hash of symbols used to set attributes of the new Bunch object. Each
|
5
|
+
# attribute is explained in more detail in its own section. The following hash keys and values
|
6
|
+
# are mandatory:
|
7
|
+
# :ndice
|
8
|
+
# :sides
|
9
|
+
# The following are optional, and modify the behaviour of the Bunch object
|
10
|
+
# :name
|
11
|
+
# :prng
|
12
|
+
# :rerolls
|
13
|
+
# :maps
|
14
|
+
# :keep_mode
|
15
|
+
# :keep_number
|
16
|
+
# Any other keys provided to the constructor are ignored
|
17
|
+
def initialize( attributes )
|
18
|
+
@name = attributes[:name].to_s
|
19
|
+
@ndice = Integer(attributes[:ndice])
|
20
|
+
raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice > 0
|
21
|
+
@sides = Integer(attributes[:sides])
|
22
|
+
raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides > 0
|
23
|
+
|
24
|
+
options = Hash.new
|
25
|
+
|
26
|
+
if attributes[:prng]
|
27
|
+
# We deliberately do not clone this object, it will often be intended that it is shared
|
28
|
+
prng = attributes[:prng]
|
29
|
+
raise ":prng does not support the rand() method" if ! prng.respond_to?(:rand)
|
65
30
|
end
|
66
31
|
|
67
|
-
|
68
|
-
attr_reader :name
|
69
|
-
|
70
|
-
# integer number of dice to roll (initially, before re-rolls etc)
|
71
|
-
attr_reader :ndice
|
72
|
-
|
73
|
-
# individual die that will be rolled, #ndice times, an GamesDice::Die or GamesDice::ComplexDie object.
|
74
|
-
attr_reader :single_die
|
75
|
-
|
76
|
-
# may be nil, :keep_best or :keep_worst
|
77
|
-
attr_reader :keep_mode
|
32
|
+
needs_complex_die = false
|
78
33
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
# after calling #roll, this is set to the final integer value from using the dice as specified
|
84
|
-
attr_reader :result
|
85
|
-
|
86
|
-
# either nil, or an array of GamesDice::RerollRule objects that are assessed on each roll of #single_die
|
87
|
-
# Reroll types :reroll_new_die and :reroll_new_keeper do not affect the #single_die, but are instead
|
88
|
-
# assessed in this container object
|
89
|
-
def rerolls
|
90
|
-
@single_die.rerolls
|
91
|
-
end
|
92
|
-
|
93
|
-
# either nil, or an array of GamesDice::MapRule objects that are assessed on each result of #single_die (after rerolls are completed)
|
94
|
-
def maps
|
95
|
-
@single_die.rerolls
|
34
|
+
if attributes[:rerolls]
|
35
|
+
needs_complex_die = true
|
36
|
+
options[:rerolls] = attributes[:rerolls].clone
|
96
37
|
end
|
97
38
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
return nil unless @raw_result_details
|
102
|
-
@raw_result_details.map { |r| r.is_a?(Fixnum) ? GamesDice::DieResult.new(r) : r }
|
39
|
+
if attributes[:maps]
|
40
|
+
needs_complex_die = true
|
41
|
+
options[:maps] = attributes[:maps].clone
|
103
42
|
end
|
104
43
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
44
|
+
if needs_complex_die
|
45
|
+
options[:prng] = prng
|
46
|
+
@single_die = GamesDice::ComplexDie.new( @sides, options )
|
47
|
+
else
|
48
|
+
@single_die = GamesDice::Die.new( @sides, prng )
|
109
49
|
end
|
110
50
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
51
|
+
case attributes[:keep_mode]
|
52
|
+
when nil then
|
53
|
+
@keep_mode = nil
|
54
|
+
when :keep_best then
|
55
|
+
@keep_mode = :keep_best
|
56
|
+
@keep_number = Integer(attributes[:keep_number] || 1)
|
57
|
+
when :keep_worst then
|
58
|
+
@keep_mode = :keep_worst
|
59
|
+
@keep_number = Integer(attributes[:keep_number] || 1)
|
60
|
+
else
|
61
|
+
raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{attributes[:keep_mode].inspect}"
|
115
62
|
end
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
63
|
+
end
|
64
|
+
|
65
|
+
# the string name as provided to the constructor, it will appear in explain_result
|
66
|
+
attr_reader :name
|
67
|
+
|
68
|
+
# integer number of dice to roll (initially, before re-rolls etc)
|
69
|
+
attr_reader :ndice
|
70
|
+
|
71
|
+
# individual die that will be rolled, #ndice times, an GamesDice::Die or GamesDice::ComplexDie object.
|
72
|
+
attr_reader :single_die
|
73
|
+
|
74
|
+
# may be nil, :keep_best or :keep_worst
|
75
|
+
attr_reader :keep_mode
|
76
|
+
|
77
|
+
# number of "best" or "worst" results to select when #keep_mode is not nil. This attribute is
|
78
|
+
# 1 by default if :keep_mode is supplied, or nil by default otherwise.
|
79
|
+
attr_reader :keep_number
|
80
|
+
|
81
|
+
# after calling #roll, this is set to the final integer value from using the dice as specified
|
82
|
+
attr_reader :result
|
83
|
+
|
84
|
+
# either nil, or an array of GamesDice::RerollRule objects that are assessed on each roll of #single_die
|
85
|
+
# Reroll types :reroll_new_die and :reroll_new_keeper do not affect the #single_die, but are instead
|
86
|
+
# assessed in this container object
|
87
|
+
def rerolls
|
88
|
+
@single_die.rerolls
|
89
|
+
end
|
90
|
+
|
91
|
+
# either nil, or an array of GamesDice::MapRule objects that are assessed on each result of #single_die (after rerolls are completed)
|
92
|
+
def maps
|
93
|
+
@single_die.rerolls
|
94
|
+
end
|
95
|
+
|
96
|
+
# after calling #roll, this is an array of GamesDice::DieResult objects, one from each #single_die rolled,
|
97
|
+
# allowing inspection of how the result was obtained.
|
98
|
+
def result_details
|
99
|
+
return nil unless @raw_result_details
|
100
|
+
@raw_result_details.map { |r| r.is_a?(Fixnum) ? GamesDice::DieResult.new(r) : r }
|
101
|
+
end
|
102
|
+
|
103
|
+
# minimum possible integer value
|
104
|
+
def min
|
105
|
+
n = @keep_mode ? [@keep_number,@ndice].min : @ndice
|
106
|
+
return n * @single_die.min
|
107
|
+
end
|
108
|
+
|
109
|
+
# maximum possible integer value
|
110
|
+
def max
|
111
|
+
n = @keep_mode ? [@keep_number,@ndice].min : @ndice
|
112
|
+
return n * @single_die.max
|
113
|
+
end
|
114
|
+
|
115
|
+
# returns a hash of value (Integer) => probability (Float) pairs. Warning: Some dice schemes
|
116
|
+
# cause this method to take a long time, and use a lot of memory. The worst-case offenders are
|
117
|
+
# dice schemes with a #keep_mode of :keep_best or :keep_worst.
|
118
|
+
def probabilities
|
119
|
+
return @probabilities if @probabilities
|
120
|
+
@probabilities_complete = true
|
121
|
+
|
122
|
+
# TODO: It is possible to optimise this slightly by combining already-calculated values
|
123
|
+
# Adding dice is same as multiplying probability sets for that number of dice
|
124
|
+
# Combine(probabililities_3_dice, probabililities_single_die) == Combine(probabililities_2_dice, probabililities_2_dice)
|
125
|
+
# It is possible to minimise the total number of multiplications, gaining about 30% efficiency, with careful choices
|
126
|
+
single_roll_probs = @single_die.probabilities.to_h
|
127
|
+
if @keep_mode && @ndice > @keep_number
|
128
|
+
preadd_probs = {}
|
129
|
+
single_roll_probs.each { |k,v| preadd_probs[k.to_s] = v }
|
130
|
+
|
131
|
+
(@keep_number-1).times do
|
132
|
+
preadd_probs = prob_accumulate_combinations preadd_probs, single_roll_probs
|
133
|
+
end
|
134
|
+
extra_dice = @ndice - @keep_number
|
135
|
+
extra_dice.times do
|
136
|
+
preadd_probs = prob_accumulate_combinations preadd_probs, single_roll_probs, @keep_mode
|
137
|
+
end
|
138
|
+
combined_probs = {}
|
139
|
+
preadd_probs.each do |k,v|
|
140
|
+
total = k.split(';').map { |s| s.to_i }.inject(:+)
|
141
|
+
combined_probs[total] ||= 0.0
|
142
|
+
combined_probs[total] += v
|
143
|
+
end
|
144
|
+
else
|
145
|
+
combined_probs = single_roll_probs.clone
|
146
|
+
(@ndice-1).times do
|
147
|
+
combined_probs = prob_accumulate combined_probs, single_roll_probs
|
151
148
|
end
|
152
|
-
|
153
|
-
@probabilities = combined_probs
|
154
|
-
@probabilities_min, @probabilities_max = @probabilities.keys.minmax
|
155
|
-
@prob_ge = {}
|
156
|
-
@prob_le = {}
|
157
|
-
@probabilities
|
158
149
|
end
|
159
150
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
end
|
151
|
+
@probabilities_min, @probabilities_max = combined_probs.keys.minmax
|
152
|
+
@probabilities = GamesDice::Probabilities.new( combined_probs )
|
153
|
+
end
|
164
154
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
155
|
+
# simulate dice roll according to spec. Returns integer final total, and also stores it in #result
|
156
|
+
def roll
|
157
|
+
@result = 0
|
158
|
+
@raw_result_details = []
|
169
159
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
return 0.0 if target > @probabilities_max
|
174
|
-
@prob_ge[target] = probabilities.select {|k,v| target <= k}.inject(0.0) {|so_far,pv| so_far + pv[1] }
|
160
|
+
@ndice.times do
|
161
|
+
@result += @single_die.roll
|
162
|
+
@raw_result_details << @single_die.result
|
175
163
|
end
|
176
164
|
|
177
|
-
|
178
|
-
|
179
|
-
target = Integer(target)
|
180
|
-
return @prob_le[target] if @prob_le && @prob_le[target]
|
181
|
-
|
182
|
-
# Force caching of probability table if not already done
|
183
|
-
probabilities
|
184
|
-
return 1.0 if target >= @probabilities_max
|
185
|
-
return 0.0 if target < @probabilities_min
|
186
|
-
@prob_le[target] = probabilities.select {|k,v| target >= k}.inject(0.0) {|so_far,pv| so_far + pv[1] }
|
165
|
+
if ! @keep_mode
|
166
|
+
return @result
|
187
167
|
end
|
188
168
|
|
189
|
-
|
190
|
-
|
191
|
-
|
169
|
+
use_dice = if @keep_mode && @keep_number < @ndice
|
170
|
+
case @keep_mode
|
171
|
+
when :keep_best then @raw_result_details.sort[-@keep_number..-1]
|
172
|
+
when :keep_worst then @raw_result_details.sort[0..(@keep_number-1)]
|
173
|
+
end
|
174
|
+
else
|
175
|
+
@raw_result_details
|
192
176
|
end
|
193
177
|
|
194
|
-
|
195
|
-
|
196
|
-
@expected_result ||= probabilities.inject(0.0) { |accumulate,p| accumulate + p[0] * p[1] }
|
197
|
-
end
|
178
|
+
@result = use_dice.inject(0) { |so_far, die_result| so_far + die_result }
|
179
|
+
end
|
198
180
|
|
199
|
-
|
200
|
-
|
201
|
-
@result = 0
|
202
|
-
@raw_result_details = []
|
181
|
+
def explain_result
|
182
|
+
return nil unless @result
|
203
183
|
|
204
|
-
|
205
|
-
@result += @single_die.roll
|
206
|
-
@raw_result_details << @single_die.result
|
207
|
-
end
|
184
|
+
explanation = ''
|
208
185
|
|
209
|
-
|
210
|
-
|
211
|
-
|
186
|
+
# With #keep_mode, we may need to show unused and used dice separately
|
187
|
+
used_dice = result_details
|
188
|
+
unused_dice = []
|
212
189
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
190
|
+
# Pick highest numbers and their associated details
|
191
|
+
if @keep_mode && @keep_number < @ndice
|
192
|
+
full_dice = result_details.sort_by { |die_result| die_result.total }
|
193
|
+
case @keep_mode
|
194
|
+
when :keep_best then
|
195
|
+
used_dice = full_dice[-@keep_number..-1]
|
196
|
+
unused_dice = full_dice[0..full_dice.length-1-@keep_number]
|
197
|
+
when :keep_worst then
|
198
|
+
used_dice = full_dice[0..(@keep_number-1)]
|
199
|
+
unused_dice = full_dice[@keep_number..(full_dice.length-1)]
|
220
200
|
end
|
221
|
-
|
222
|
-
@result = use_dice.inject(0) { |so_far, die_result| so_far + die_result }
|
223
201
|
end
|
224
202
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
if @keep_mode && @keep_number < @ndice
|
236
|
-
full_dice = result_details.sort_by { |die_result| die_result.total }
|
237
|
-
case @keep_mode
|
238
|
-
when :keep_best then
|
239
|
-
used_dice = full_dice[-@keep_number..-1]
|
240
|
-
unused_dice = full_dice[0..full_dice.length-1-@keep_number]
|
241
|
-
when :keep_worst then
|
242
|
-
used_dice = full_dice[0..(@keep_number-1)]
|
243
|
-
unused_dice = full_dice[@keep_number..(full_dice.length-1)]
|
244
|
-
end
|
203
|
+
# Show unused dice (if any)
|
204
|
+
if @keep_mode || @single_die.maps
|
205
|
+
explanation += result_details.map do |die_result|
|
206
|
+
die_result.explain_value
|
207
|
+
end.join(', ')
|
208
|
+
if @keep_mode
|
209
|
+
separator = @single_die.maps ? ', ' : ' + '
|
210
|
+
explanation += ". Keep: " + used_dice.map do |die_result|
|
211
|
+
die_result.explain_total
|
212
|
+
end.join( separator )
|
245
213
|
end
|
246
|
-
|
247
|
-
|
248
|
-
if @keep_mode || @single_die.maps
|
249
|
-
explanation += result_details.map do |die_result|
|
250
|
-
die_result.explain_value
|
251
|
-
end.join(', ')
|
252
|
-
if @keep_mode
|
253
|
-
separator = @single_die.maps ? ', ' : ' + '
|
254
|
-
explanation += ". Keep: " + used_dice.map do |die_result|
|
255
|
-
die_result.explain_total
|
256
|
-
end.join( separator )
|
257
|
-
end
|
258
|
-
if @single_die.maps
|
259
|
-
explanation += ". Successes: #{@result}"
|
260
|
-
end
|
261
|
-
explanation += " = #{@result}" if @keep_mode && ! @single_die.maps && @keep_number > 1
|
262
|
-
else
|
263
|
-
explanation += used_dice.map do |die_result|
|
264
|
-
die_result.explain_value
|
265
|
-
end.join(' + ')
|
266
|
-
explanation += " = #{@result}" if @ndice > 1
|
214
|
+
if @single_die.maps
|
215
|
+
explanation += ". Successes: #{@result}"
|
267
216
|
end
|
268
|
-
|
269
|
-
|
217
|
+
explanation += " = #{@result}" if @keep_mode && ! @single_die.maps && @keep_number > 1
|
218
|
+
else
|
219
|
+
explanation += used_dice.map do |die_result|
|
220
|
+
die_result.explain_value
|
221
|
+
end.join(' + ')
|
222
|
+
explanation += " = #{@result}" if @ndice > 1
|
270
223
|
end
|
271
224
|
|
272
|
-
|
225
|
+
explanation
|
226
|
+
end
|
273
227
|
|
274
|
-
|
275
|
-
# the second set of keys, at the associated probailities of the values
|
276
|
-
def prob_accumulate first_probs, second_probs
|
277
|
-
accumulator = Hash.new
|
228
|
+
private
|
278
229
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
accumulator[v3] ||= 0.0
|
284
|
-
accumulator[v3] += p3
|
285
|
-
end
|
286
|
-
end
|
230
|
+
# combines two sets of probabilities where the end result is the first set of keys plus
|
231
|
+
# the second set of keys, at the associated probailities of the values
|
232
|
+
def prob_accumulate first_probs, second_probs
|
233
|
+
accumulator = Hash.new
|
287
234
|
|
288
|
-
|
235
|
+
first_probs.each do |v1,p1|
|
236
|
+
second_probs.each do |v2,p2|
|
237
|
+
v3 = v1 + v2
|
238
|
+
p3 = p1 * p2
|
239
|
+
accumulator[v3] ||= 0.0
|
240
|
+
accumulator[v3] += p3
|
241
|
+
end
|
289
242
|
end
|
290
243
|
|
291
|
-
|
292
|
-
|
293
|
-
accumulator = Hash.new
|
244
|
+
accumulator
|
245
|
+
end
|
294
246
|
|
295
|
-
|
296
|
-
|
247
|
+
# combines two sets of probabilities, as above, except tracking unique permutations
|
248
|
+
def prob_accumulate_combinations so_far, die_probs, keep_rule = nil
|
249
|
+
accumulator = Hash.new
|
297
250
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
accumulator[new_sig] += p3
|
251
|
+
so_far.each do |sig,p1|
|
252
|
+
combo = sig.split(';').map { |s| s.to_i }
|
253
|
+
|
254
|
+
case keep_rule
|
255
|
+
when nil then
|
256
|
+
die_probs.each do |v2,p2|
|
257
|
+
new_sig = (combo + [v2]).sort.join(';')
|
258
|
+
p3 = p1 * p2
|
259
|
+
accumulator[new_sig] ||= 0.0
|
260
|
+
accumulator[new_sig] += p3
|
261
|
+
end
|
262
|
+
when :keep_best then
|
263
|
+
need_more_than = combo.min
|
264
|
+
die_probs.each do |v2,p2|
|
265
|
+
if v2 > need_more_than
|
266
|
+
new_sig = (combo + [v2]).sort[1..combo.size].join(';')
|
267
|
+
else
|
268
|
+
new_sig = sig
|
317
269
|
end
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
270
|
+
p3 = p1 * p2
|
271
|
+
accumulator[new_sig] ||= 0.0
|
272
|
+
accumulator[new_sig] += p3
|
273
|
+
end
|
274
|
+
when :keep_worst then
|
275
|
+
need_less_than = combo.max
|
276
|
+
die_probs.each do |v2,p2|
|
277
|
+
if v2 < need_less_than
|
278
|
+
new_sig = (combo + [v2]).sort[0..(combo.size-1)].join(';')
|
279
|
+
else
|
280
|
+
new_sig = sig
|
329
281
|
end
|
282
|
+
p3 = p1 * p2
|
283
|
+
accumulator[new_sig] ||= 0.0
|
284
|
+
accumulator[new_sig] += p3
|
330
285
|
end
|
331
286
|
end
|
332
|
-
|
333
|
-
accumulator
|
334
287
|
end
|
335
288
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
289
|
+
accumulator
|
290
|
+
end
|
291
|
+
|
292
|
+
# Generates all sets of [throw_away,may_keep_exactly,keep_preferentially,combinations] that meet
|
293
|
+
# criteria for correct total number of dice and keep dice. These then need to be assessed for every
|
294
|
+
# die value by the caller to get a full set of probabilities
|
295
|
+
def generate_item_counts total_dice, keep_dice
|
296
|
+
# Constraints are:
|
297
|
+
# may_keep_exactly must be at least 1, and at most is all the dice
|
298
|
+
# keep_preferentially plus may_keep_exactly must be >= keep_dice, but keep_preferentially < keep dice
|
299
|
+
# sum of all three always == total_dice
|
300
|
+
item_counts = []
|
301
|
+
(1..total_dice).each do |may_keep_exactly|
|
302
|
+
min_kp = [keep_dice - may_keep_exactly, 0].max
|
303
|
+
max_kp = [keep_dice - 1, total_dice - may_keep_exactly].min
|
304
|
+
(min_kp..max_kp).each do |keep_preferentially|
|
305
|
+
counts = [ total_dice - may_keep_exactly - keep_preferentially, may_keep_exactly, keep_preferentially ]
|
306
|
+
counts << combinations(counts)
|
307
|
+
item_counts << counts
|
353
308
|
end
|
354
|
-
item_counts
|
355
309
|
end
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
end
|
367
|
-
|
310
|
+
item_counts
|
311
|
+
end
|
312
|
+
|
313
|
+
# How many unique ways can a set of items, some of which are identical, be arranged?
|
314
|
+
def combinations item_counts
|
315
|
+
item_counts = item_counts.map { |i| Integer(i) }.select { |i| i > 0 }
|
316
|
+
total_items = item_counts.inject(:+)
|
317
|
+
numerator = 1.upto(total_items).inject(:*)
|
318
|
+
denominator = item_counts.map { |i| 1.upto(i).inject(:*) }.inject(:*)
|
319
|
+
numerator / denominator
|
320
|
+
end
|
321
|
+
|
322
|
+
end # class Bunch
|