games_dice 0.0.3 → 0.0.5
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/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
|