games_dice 0.3.7 → 0.3.8
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 +4 -0
- data/README.md +4 -1
- data/ext/games_dice/probabilities.c +8 -4
- data/games_dice.gemspec +2 -0
- data/lib/games_dice.rb +1 -0
- data/lib/games_dice/bunch.rb +39 -38
- data/lib/games_dice/complex_die.rb +79 -71
- data/lib/games_dice/dice.rb +15 -16
- data/lib/games_dice/die.rb +13 -0
- data/lib/games_dice/marshal.rb +13 -0
- data/lib/games_dice/probabilities.rb +54 -53
- data/lib/games_dice/version.rb +1 -1
- data/spec/complex_die_spec.rb +2 -2
- data/spec/die_spec.rb +16 -0
- data/spec/fixtures/probs_fair_die_6.dat +2 -0
- data/spec/helpers.rb +9 -1
- data/spec/probability_spec.rb +8 -0
- metadata +143 -122
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# GamesDice
|
2
|
-
|
2
|
+
[](http://badge.fury.io/rb/games_dice)
|
3
3
|
[](http://travis-ci.org/neilslater/games_dice)
|
4
|
+
[](https://coveralls.io/r/neilslater/games_dice?branch=master)
|
5
|
+
[](https://codeclimate.com/github/neilslater/games_dice)
|
6
|
+
[](https://gemnasium.com/neilslater/games_dice)
|
4
7
|
|
5
8
|
A library for simulating dice. Use it to construct dice-rolling systems used in role-playing and board games.
|
6
9
|
|
@@ -41,7 +41,7 @@ inline int min( int *a, int n ) {
|
|
41
41
|
|
42
42
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
43
43
|
//
|
44
|
-
// Quick factorials, that fit into
|
44
|
+
// Quick factorials, that fit into doubles. . . the size of this structure sets the
|
45
45
|
// maximum possible n in repeat_n_sum_k calculations
|
46
46
|
//
|
47
47
|
|
@@ -744,11 +744,15 @@ VALUE probabilities_for_fair_die( VALUE self, VALUE sides ) {
|
|
744
744
|
}
|
745
745
|
|
746
746
|
VALUE probabilities_from_h( VALUE self, VALUE hash ) {
|
747
|
+
VALUE obj;
|
748
|
+
ProbabilityList *pl;
|
749
|
+
double error;
|
750
|
+
|
747
751
|
Check_Type( hash, T_HASH );
|
748
752
|
|
749
|
-
|
750
|
-
|
751
|
-
|
753
|
+
obj = pl_alloc( Probabilities );
|
754
|
+
pl = get_probability_list( obj );
|
755
|
+
|
752
756
|
|
753
757
|
// Set these up so that they get adjusted during hash iteration
|
754
758
|
pl->offset = 0x7fffffff;
|
data/games_dice.gemspec
CHANGED
@@ -17,6 +17,8 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.add_development_dependency "rspec", ">= 2.13.0"
|
18
18
|
gem.add_development_dependency "rake", ">= 1.9.1"
|
19
19
|
gem.add_development_dependency "yard", ">= 0.8.6"
|
20
|
+
gem.add_development_dependency "coveralls", ">= 0.6.7"
|
21
|
+
gem.add_development_dependency "json", ">= 1.7.7"
|
20
22
|
gem.add_development_dependency "rake-compiler"
|
21
23
|
|
22
24
|
# Red Carpet renders README.md, and is optional even when developing the gem.
|
data/lib/games_dice.rb
CHANGED
data/lib/games_dice/bunch.rb
CHANGED
@@ -32,50 +32,17 @@ class GamesDice::Bunch
|
|
32
32
|
# @option options [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
|
33
33
|
# @return [GamesDice::Bunch]
|
34
34
|
def initialize( options )
|
35
|
-
|
36
|
-
|
37
|
-
raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice > 0
|
38
|
-
@sides = Integer(options[:sides])
|
39
|
-
raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides > 0
|
40
|
-
|
41
|
-
attr = Hash.new
|
35
|
+
name_number_sides_from_hash( options )
|
36
|
+
keep_mode_from_hash( options )
|
42
37
|
|
43
38
|
if options[:prng]
|
44
|
-
# We deliberately do not clone this object, it will often be intended that it is shared
|
45
|
-
prng = options[:prng]
|
46
39
|
raise ":prng does not support the rand() method" if ! prng.respond_to?(:rand)
|
47
40
|
end
|
48
41
|
|
49
|
-
|
50
|
-
|
51
|
-
if options[:rerolls]
|
52
|
-
needs_complex_die = true
|
53
|
-
attr[:rerolls] = options[:rerolls].clone
|
54
|
-
end
|
55
|
-
|
56
|
-
if options[:maps]
|
57
|
-
needs_complex_die = true
|
58
|
-
attr[:maps] = options[:maps].clone
|
59
|
-
end
|
60
|
-
|
61
|
-
if needs_complex_die
|
62
|
-
attr[:prng] = prng
|
63
|
-
@single_die = GamesDice::ComplexDie.new( @sides, attr )
|
64
|
-
else
|
65
|
-
@single_die = GamesDice::Die.new( @sides, prng )
|
66
|
-
end
|
67
|
-
|
68
|
-
case options[:keep_mode]
|
69
|
-
when nil then
|
70
|
-
@keep_mode = nil
|
71
|
-
when :keep_best then
|
72
|
-
@keep_mode = :keep_best
|
73
|
-
@keep_number = Integer(options[:keep_number] || 1)
|
74
|
-
when :keep_worst then
|
75
|
-
@keep_mode = :keep_worst
|
76
|
-
@keep_number = Integer(options[:keep_number] || 1)
|
42
|
+
if options[:rerolls] || options[:maps]
|
43
|
+
@single_die = GamesDice::ComplexDie.new( @sides, complex_die_params_from_hash( options ) )
|
77
44
|
else
|
78
|
-
|
45
|
+
@single_die = GamesDice::Die.new( @sides, options[:prng] )
|
79
46
|
end
|
80
47
|
end
|
81
48
|
|
@@ -243,4 +210,38 @@ class GamesDice::Bunch
|
|
243
210
|
explanation
|
244
211
|
end
|
245
212
|
|
213
|
+
private
|
214
|
+
|
215
|
+
def name_number_sides_from_hash options
|
216
|
+
@name = options[:name].to_s
|
217
|
+
@ndice = Integer(options[:ndice])
|
218
|
+
raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice > 0
|
219
|
+
@sides = Integer(options[:sides])
|
220
|
+
raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides > 0
|
221
|
+
end
|
222
|
+
|
223
|
+
def keep_mode_from_hash options
|
224
|
+
case options[:keep_mode]
|
225
|
+
when nil then
|
226
|
+
@keep_mode = nil
|
227
|
+
when :keep_best then
|
228
|
+
@keep_mode = :keep_best
|
229
|
+
@keep_number = Integer(options[:keep_number] || 1)
|
230
|
+
when :keep_worst then
|
231
|
+
@keep_mode = :keep_worst
|
232
|
+
@keep_number = Integer(options[:keep_number] || 1)
|
233
|
+
else
|
234
|
+
raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{options[:keep_mode].inspect}"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def complex_die_params_from_hash options
|
239
|
+
cd_hash = Hash.new
|
240
|
+
[:maps,:rerolls].each do |k|
|
241
|
+
cd_hash[k] = options[k].clone if options[k]
|
242
|
+
end
|
243
|
+
# We deliberately do not clone this object, it will often be intended that it is shared
|
244
|
+
cd_hash[:prng] = options[:prng]
|
245
|
+
cd_hash
|
246
|
+
end
|
246
247
|
end # class Bunch
|
@@ -60,7 +60,7 @@ class GamesDice::ComplexDie
|
|
60
60
|
# True if all possible results are represented and assigned a probability. Dice with open-ended re-rolls
|
61
61
|
# may have calculations cut short, and will result in a false value of this attribute. Even when this
|
62
62
|
# attribute is false, probabilities should still be accurate to nearest 1e-9.
|
63
|
-
# @return [Boolean, nil] Depending on completeness when generating #
|
63
|
+
# @return [Boolean, nil] Depending on completeness when generating #probabilities
|
64
64
|
attr_reader :probabilities_complete
|
65
65
|
|
66
66
|
# @!attribute [r] sides
|
@@ -81,10 +81,7 @@ class GamesDice::ComplexDie
|
|
81
81
|
# @return [Integer]
|
82
82
|
def min
|
83
83
|
return @min_result if @min_result
|
84
|
-
|
85
|
-
return @min_result if @probabilities_complete
|
86
|
-
logical_min, logical_max = logical_minmax
|
87
|
-
@min_result, @max_result = [@min_result, @max_result, logical_min, logical_max].minmax
|
84
|
+
calc_minmax
|
88
85
|
@min_result
|
89
86
|
end
|
90
87
|
|
@@ -92,10 +89,7 @@ class GamesDice::ComplexDie
|
|
92
89
|
# @return [Integer] Maximum possible result from a call to #roll
|
93
90
|
def max
|
94
91
|
return @max_result if @max_result
|
95
|
-
|
96
|
-
return @max_result if @probabilities_complete
|
97
|
-
logical_min, logical_max = logical_minmax
|
98
|
-
@min_result, @max_result = [@min_result, @max_result, logical_min, logical_max].minmax
|
92
|
+
calc_minmax
|
99
93
|
@max_result
|
100
94
|
end
|
101
95
|
|
@@ -134,66 +128,70 @@ class GamesDice::ComplexDie
|
|
134
128
|
# @param [Symbol] reason Assign a reason for rolling the first die.
|
135
129
|
# @return [GamesDice::DieResult] Detailed results from rolling the die, including resolution of rules.
|
136
130
|
def roll( reason = :basic )
|
137
|
-
# Important bit - actually roll the die
|
138
131
|
@result = GamesDice::DieResult.new( @basic_die.roll, reason )
|
132
|
+
roll_apply_rerolls
|
133
|
+
roll_apply_maps
|
134
|
+
@result
|
135
|
+
end
|
139
136
|
|
140
|
-
|
141
|
-
subtracting = false
|
142
|
-
rerolls_remaining = @rerolls.map { |rule| rule.limit }
|
143
|
-
loop do
|
144
|
-
# Find which rule, if any, is being triggered
|
145
|
-
rule_idx = @rerolls.zip(rerolls_remaining).find_index do |rule,remaining|
|
146
|
-
next if rule.type == :reroll_subtract && @result.rolls.length > 1
|
147
|
-
remaining > 0 && rule.applies?( @basic_die.result )
|
148
|
-
end
|
149
|
-
break unless rule_idx
|
137
|
+
private
|
150
138
|
|
151
|
-
|
152
|
-
|
153
|
-
|
139
|
+
def roll_apply_rerolls
|
140
|
+
return unless @rerolls
|
141
|
+
subtracting = false
|
142
|
+
rerolls_remaining = @rerolls.map { |rule| rule.limit }
|
154
143
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
end
|
144
|
+
loop do
|
145
|
+
# Find which rule, if any, is being triggered
|
146
|
+
rule_idx = @rerolls.zip(rerolls_remaining).find_index do |rule,remaining|
|
147
|
+
next if rule.type == :reroll_subtract && @result.rolls.length > 1
|
148
|
+
remaining > 0 && rule.applies?( @basic_die.result )
|
161
149
|
end
|
162
|
-
|
150
|
+
break unless rule_idx
|
151
|
+
|
152
|
+
rule = @rerolls[ rule_idx ]
|
153
|
+
rerolls_remaining[ rule_idx ] -= 1
|
154
|
+
subtracting = true if rule.type == :reroll_subtract
|
163
155
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
156
|
+
# Apply the rule (note reversal for additions, after a subtract)
|
157
|
+
if subtracting && rule.type == :reroll_add
|
158
|
+
@result.add_roll( @basic_die.roll, :reroll_subtract )
|
159
|
+
else
|
160
|
+
@result.add_roll( @basic_die.roll, rule.type )
|
161
|
+
end
|
168
162
|
end
|
163
|
+
end
|
169
164
|
|
170
|
-
|
165
|
+
def roll_apply_maps
|
166
|
+
return unless @maps
|
167
|
+
m, n = calc_maps(@result.value)
|
168
|
+
@result.apply_map( m, n )
|
171
169
|
end
|
172
170
|
|
173
|
-
|
171
|
+
def calc_minmax
|
172
|
+
@min_result, @max_result = [probabilities.min, probabilities.max]
|
173
|
+
return if @probabilities_complete
|
174
|
+
logical_min, logical_max = logical_minmax
|
175
|
+
@min_result, @max_result = [@min_result, @max_result, logical_min, logical_max].minmax
|
176
|
+
end
|
174
177
|
|
175
178
|
def construct_rerolls rerolls_input
|
176
|
-
|
177
|
-
raise TypeError, "rerolls should be an Array, instead got #{rerolls_input.inspect}" unless rerolls_input.is_a?(Array)
|
178
|
-
rerolls_input.map do |reroll_item|
|
179
|
-
case reroll_item
|
180
|
-
when Array then GamesDice::RerollRule.new( reroll_item[0], reroll_item[1], reroll_item[2], reroll_item[3] )
|
181
|
-
when GamesDice::RerollRule then reroll_item
|
182
|
-
else
|
183
|
-
raise TypeError, "items in rerolls should be GamesDice::RerollRule or Array, instead got #{reroll_item.inspect}"
|
184
|
-
end
|
185
|
-
end
|
179
|
+
check_and_construct rerolls_input, GamesDice::RerollRule, 'rerolls'
|
186
180
|
end
|
187
181
|
|
188
182
|
def construct_maps maps_input
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
183
|
+
check_and_construct maps_input, GamesDice::MapRule, 'maps'
|
184
|
+
end
|
185
|
+
|
186
|
+
def check_and_construct input, klass, label
|
187
|
+
return nil unless input
|
188
|
+
raise TypeError, "#{label} should be an Array, instead got #{input.inspect}" unless input.is_a?(Array)
|
189
|
+
input.map do |i|
|
190
|
+
case i
|
191
|
+
when Array then klass.new( *i )
|
192
|
+
when klass then i
|
195
193
|
else
|
196
|
-
raise TypeError, "items in
|
194
|
+
raise TypeError, "items in #{label} should be #{klass.name} or Array, instead got #{i.inspect}"
|
197
195
|
end
|
198
196
|
end
|
199
197
|
end
|
@@ -217,27 +215,37 @@ class GamesDice::ComplexDie
|
|
217
215
|
|
218
216
|
# This isn't 100% accurate, but does cover most "normal" scenarios, and we're only falling back to it when we have to
|
219
217
|
def logical_minmax
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
return minmax_mappings( (min_result..max_result) )
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
218
|
+
return [@basic_die.min,@basic_die.max] unless @rerolls || @maps
|
219
|
+
return minmax_mappings( @basic_die.all_values ) unless @rerolls
|
220
|
+
min_result, max_result = logical_rerolls_minmax
|
221
|
+
return minmax_mappings( (min_result..max_result) ) if @maps
|
222
|
+
return [min_result,max_result]
|
223
|
+
end
|
224
|
+
|
225
|
+
def logical_rerolls_minmax
|
226
|
+
min_result = @basic_die.min
|
227
|
+
max_result = @basic_die.max
|
228
|
+
min_subtract, max_add = find_add_subtract_extremes
|
229
|
+
if min_subtract
|
230
|
+
min_result = [ min_subtract - max_add, min_subtract - max_result ].min
|
231
|
+
end
|
232
|
+
[ min_result, max_add + max_result ]
|
233
|
+
end
|
234
|
+
|
235
|
+
def find_add_subtract_extremes
|
236
|
+
min_subtract = nil
|
237
|
+
total_add = 0
|
238
|
+
@rerolls.select {|r| [:reroll_add, :reroll_subtract].member?( r.type ) }.each do |rule|
|
239
|
+
min_reroll,max_reroll = @basic_die.all_values.select { |v| rule.applies?( v ) }.minmax
|
240
|
+
next unless min_reroll
|
229
241
|
if rule.type == :reroll_subtract
|
230
|
-
|
231
|
-
|
242
|
+
min_subtract = min_reroll if min_subtract.nil?
|
243
|
+
min_subtract = min_reroll if min_subtract > min_reroll
|
232
244
|
else
|
233
|
-
|
245
|
+
total_add += max_reroll * rule.limit
|
234
246
|
end
|
235
247
|
end
|
236
|
-
|
237
|
-
min_result -= max_result + @basic_die.sides
|
238
|
-
end
|
239
|
-
return minmax_mappings( (min_result..max_result) ) if @maps
|
240
|
-
return [min_result,max_result]
|
248
|
+
[ min_subtract, total_add ]
|
241
249
|
end
|
242
250
|
|
243
251
|
def recursive_probabilities probabilities={},prior_probability=1.0,depth=0,prior_result=nil,rerolls_left=nil,roll_reason=:basic,subtracting=false
|
@@ -248,7 +256,7 @@ class GamesDice::ComplexDie
|
|
248
256
|
stop_recursing = true
|
249
257
|
end
|
250
258
|
|
251
|
-
|
259
|
+
@basic_die.each_value do |v|
|
252
260
|
# calculate value, recurse if there is a reroll
|
253
261
|
result_so_far, rerolls_remaining = calc_result_so_far(prior_result, rerolls_left, v, roll_reason )
|
254
262
|
|
data/lib/games_dice/dice.rb
CHANGED
@@ -63,30 +63,21 @@ class GamesDice::Dice
|
|
63
63
|
# Simulates rolling dice
|
64
64
|
# @return [Integer] Sum of all rolled dice
|
65
65
|
def roll
|
66
|
-
@result = @offset +
|
67
|
-
m,b = mb
|
68
|
-
total += m * b.roll
|
69
|
-
end
|
66
|
+
@result = @offset + bunches_weighted_sum( :roll )
|
70
67
|
end
|
71
68
|
|
72
69
|
# @!attribute [r] min
|
73
70
|
# Minimum possible result from a call to #roll
|
74
71
|
# @return [Integer]
|
75
72
|
def min
|
76
|
-
@min ||= @offset +
|
77
|
-
m,b = mb
|
78
|
-
total += m * b.min
|
79
|
-
end
|
73
|
+
@min ||= @offset + bunches_weighted_sum( :min )
|
80
74
|
end
|
81
75
|
|
82
76
|
# @!attribute [r] max
|
83
77
|
# Maximum possible result from a call to #roll
|
84
78
|
# @return [Integer]
|
85
79
|
def max
|
86
|
-
@max ||= @offset +
|
87
|
-
m,b = mb
|
88
|
-
total += m * b.max
|
89
|
-
end
|
80
|
+
@max ||= @offset + bunches_weighted_sum( :max )
|
90
81
|
end
|
91
82
|
|
92
83
|
# @!attribute [r] minmax
|
@@ -135,10 +126,18 @@ class GamesDice::Dice
|
|
135
126
|
private
|
136
127
|
|
137
128
|
def array_to_sum array
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
129
|
+
( numbers_to_strings(array) + [ '=', array.inject(:+) ] ).join(' ')
|
130
|
+
end
|
131
|
+
|
132
|
+
def numbers_to_strings array
|
133
|
+
[ array.first.to_s ] + array.drop(1).map { |n| n < 0 ? '- ' + n.abs.to_s : '+ ' + n.to_s }
|
134
|
+
end
|
135
|
+
|
136
|
+
def bunches_weighted_sum summed_method
|
137
|
+
@bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
|
138
|
+
m,b = mb
|
139
|
+
total += m * b.send( summed_method )
|
140
|
+
end
|
142
141
|
end
|
143
142
|
|
144
143
|
end # class Dice
|
data/lib/games_dice/die.rb
CHANGED
@@ -68,6 +68,19 @@ class GamesDice::Die
|
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
+
# Iterates through all possible results on die.
|
72
|
+
# @yieldparam [Integer] result A potential result from the die
|
73
|
+
# @return [GamesDice::Die] this object
|
74
|
+
def each_value
|
75
|
+
(1..@sides).each { |r| yield(r) }
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Array<Integer>] All potential results from the die
|
80
|
+
def all_values
|
81
|
+
(1..@sides).to_a
|
82
|
+
end
|
83
|
+
|
71
84
|
# @!attribute [r] rerolls
|
72
85
|
# Rules for when to re-roll this die.
|
73
86
|
# @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class GamesDice::Probabilities
|
2
|
+
# @!visibility private
|
3
|
+
# Adds support for Marshal, via to_h and from_h methods
|
4
|
+
def _dump *ignored
|
5
|
+
Marshal.dump to_h
|
6
|
+
end
|
7
|
+
|
8
|
+
# @!visibility private
|
9
|
+
def self._load buf
|
10
|
+
h = Marshal.load buf
|
11
|
+
from_h h
|
12
|
+
end
|
13
|
+
end
|
@@ -183,18 +183,8 @@ class GamesDice::Probabilities
|
|
183
183
|
|
184
184
|
combined_min = pd_a.min + pd_b.min
|
185
185
|
combined_max = pd_a.max + pd_b.max
|
186
|
-
new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
|
187
|
-
probs_a, offset_a = pd_a.to_ao
|
188
|
-
probs_b, offset_b = pd_b.to_ao
|
189
186
|
|
190
|
-
|
191
|
-
probs_b.each_with_index do |pb,j|
|
192
|
-
k = i + j
|
193
|
-
pc = pa * pb
|
194
|
-
new_probs[ k ] += pc
|
195
|
-
end
|
196
|
-
end
|
197
|
-
GamesDice::Probabilities.new( new_probs, combined_min )
|
187
|
+
add_distributions_internal combined_min, combined_max, 1, pd_a, 1, pd_b
|
198
188
|
end
|
199
189
|
|
200
190
|
# Combines two distributions with multipliers to create a third, that represents the distribution
|
@@ -217,18 +207,7 @@ class GamesDice::Probabilities
|
|
217
207
|
m_a * pd_a.min + m_b * pd_b.max, m_a * pd_a.max + m_b * pd_b.max,
|
218
208
|
].minmax
|
219
209
|
|
220
|
-
|
221
|
-
probs_a, offset_a = pd_a.to_ao
|
222
|
-
probs_b, offset_b = pd_b.to_ao
|
223
|
-
|
224
|
-
probs_a.each_with_index do |pa,i|
|
225
|
-
probs_b.each_with_index do |pb,j|
|
226
|
-
k = m_a * (i + offset_a) + m_b * (j + offset_b) - combined_min
|
227
|
-
pc = pa * pb
|
228
|
-
new_probs[ k ] += pc
|
229
|
-
end
|
230
|
-
end
|
231
|
-
GamesDice::Probabilities.new( new_probs, combined_min )
|
210
|
+
add_distributions_internal combined_min, combined_max, m_a, pd_a, m_b, pd_b
|
232
211
|
end
|
233
212
|
|
234
213
|
# Returns a symbol for the language name that this class is implemented in. The C version of the
|
@@ -246,20 +225,21 @@ class GamesDice::Probabilities
|
|
246
225
|
n = Integer( n )
|
247
226
|
raise "Cannot combine probabilities less than once" if n < 1
|
248
227
|
raise "Probability distribution too large" if ( n * @probs.count ) > 1000000
|
249
|
-
revbin = n.to_s(2).reverse.each_char.to_a.map { |c| c == '1' }
|
250
228
|
pd_power = self
|
251
229
|
pd_result = nil
|
252
|
-
max_power = revbin.count - 1
|
253
230
|
|
254
|
-
|
255
|
-
|
231
|
+
use_power = 1
|
232
|
+
loop do
|
233
|
+
if ( use_power & n ) > 0
|
256
234
|
if pd_result
|
257
235
|
pd_result = GamesDice::Probabilities.add_distributions( pd_result, pd_power )
|
258
236
|
else
|
259
237
|
pd_result = pd_power
|
260
238
|
end
|
261
239
|
end
|
262
|
-
|
240
|
+
use_power = use_power << 1
|
241
|
+
break if use_power > n
|
242
|
+
pd_power = GamesDice::Probabilities.add_distributions( pd_power, pd_power )
|
263
243
|
end
|
264
244
|
pd_result
|
265
245
|
end
|
@@ -275,6 +255,8 @@ class GamesDice::Probabilities
|
|
275
255
|
raise "Cannot combine probabilities less than once" if n < 1
|
276
256
|
# Technically this is a limitation of C code, but Ruby version is most likely slow and inaccurate beyond 170
|
277
257
|
raise "Too many dice to calculate numbers of arrangements" if n > 170
|
258
|
+
check_keep_mode( kmode )
|
259
|
+
|
278
260
|
if k >= n
|
279
261
|
return repeat_sum( n )
|
280
262
|
end
|
@@ -309,6 +291,21 @@ class GamesDice::Probabilities
|
|
309
291
|
|
310
292
|
private
|
311
293
|
|
294
|
+
def self.add_distributions_internal combined_min, combined_max, m_a, pd_a, m_b, pd_b
|
295
|
+
new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
|
296
|
+
probs_a, offset_a = pd_a.to_ao
|
297
|
+
probs_b, offset_b = pd_b.to_ao
|
298
|
+
|
299
|
+
probs_a.each_with_index do |pa,i|
|
300
|
+
probs_b.each_with_index do |pb,j|
|
301
|
+
k = m_a * (i + offset_a) + m_b * (j + offset_b) - combined_min
|
302
|
+
pc = pa * pb
|
303
|
+
new_probs[ k ] += pc
|
304
|
+
end
|
305
|
+
end
|
306
|
+
GamesDice::Probabilities.new( new_probs, combined_min )
|
307
|
+
end
|
308
|
+
|
312
309
|
def check_probs_array probs_array
|
313
310
|
raise TypeError unless probs_array.is_a?( Array )
|
314
311
|
probs_array.map!{ |n| Float(n) }
|
@@ -325,41 +322,41 @@ class GamesDice::Probabilities
|
|
325
322
|
end
|
326
323
|
|
327
324
|
def calc_keep_distributions k, q, kmode
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
end
|
325
|
+
kd_probabilities = calc_keep_definite_distributions q, kmode
|
326
|
+
|
327
|
+
keep_distributions = [ GamesDice::Probabilities.new( [1.0], q * k ) ]
|
328
|
+
if kd_probabilities && k > 1
|
329
|
+
(1...k).each do |n|
|
330
|
+
extra_o = GamesDice::Probabilities.new( [1.0], q * ( k - n ) )
|
331
|
+
n_probs = kd_probabilities.repeat_sum( n )
|
332
|
+
keep_distributions[n] = GamesDice::Probabilities.add_distributions( extra_o, n_probs )
|
337
333
|
end
|
338
|
-
elsif kmode == :keep_worst
|
339
|
-
keep_distributions = [ GamesDice::Probabilities.new( [1.0], q * k ) ]
|
340
|
-
if p_lt(q) > 0.0 && k > 1
|
341
|
-
kd_probabilities = given_le( q - 1 )
|
342
|
-
(1...k).each do |n|
|
343
|
-
extra_o = GamesDice::Probabilities.new( [1.0], q * ( k - n ) )
|
344
|
-
n_probs = kd_probabilities.repeat_sum( n )
|
345
|
-
keep_distributions[n] = GamesDice::Probabilities.add_distributions( extra_o, n_probs )
|
346
|
-
end
|
347
|
-
end
|
348
|
-
else
|
349
|
-
raise "Keep mode #{kmode.inspect} not recognised"
|
350
334
|
end
|
335
|
+
|
351
336
|
keep_distributions
|
352
337
|
end
|
353
338
|
|
339
|
+
def calc_keep_definite_distributions q, kmode
|
340
|
+
kd_probabilities = nil
|
341
|
+
case kmode
|
342
|
+
when :keep_best
|
343
|
+
p_definites = p_gt(q)
|
344
|
+
kd_probabilities = given_ge( q + 1 ) if p_definites > 0.0
|
345
|
+
when :keep_worst
|
346
|
+
p_definites = p_lt(q)
|
347
|
+
kd_probabilities = given_le( q - 1 ) if p_definites > 0.0
|
348
|
+
end
|
349
|
+
kd_probabilities
|
350
|
+
end
|
351
|
+
|
354
352
|
def calc_p_table q, p_maybe, kmode
|
355
|
-
|
353
|
+
case kmode
|
354
|
+
when :keep_best
|
356
355
|
p_kept = p_gt(q)
|
357
356
|
p_rejected = p_lt(q)
|
358
|
-
|
357
|
+
when :keep_worst
|
359
358
|
p_kept = p_lt(q)
|
360
359
|
p_rejected = p_gt(q)
|
361
|
-
else
|
362
|
-
raise "Keep mode #{kmode.inspect} not recognised"
|
363
360
|
end
|
364
361
|
[ p_rejected, p_maybe, p_kept ]
|
365
362
|
end
|
@@ -388,6 +385,10 @@ class GamesDice::Probabilities
|
|
388
385
|
total
|
389
386
|
end
|
390
387
|
|
388
|
+
def check_keep_mode kmode
|
389
|
+
raise "Keep mode #{kmode.inspect} not recognised" unless [:keep_best,:keep_worst].member?( kmode )
|
390
|
+
end
|
391
|
+
|
391
392
|
end # class GamesDice::Probabilities
|
392
393
|
|
393
394
|
# @!visibility private
|
data/lib/games_dice/version.rb
CHANGED
data/spec/complex_die_spec.rb
CHANGED
@@ -57,7 +57,7 @@ describe GamesDice::ComplexDie do
|
|
57
57
|
|
58
58
|
lambda do
|
59
59
|
GamesDice::ComplexDie.new( 10, :rerolls => [['hello']] )
|
60
|
-
end.should raise_error(
|
60
|
+
end.should raise_error( ArgumentError )
|
61
61
|
|
62
62
|
lambda do
|
63
63
|
GamesDice::ComplexDie.new( 10, :rerolls => [ [6, :<=, :reroll_add ], :reroll_add] )
|
@@ -82,7 +82,7 @@ describe GamesDice::ComplexDie do
|
|
82
82
|
|
83
83
|
lambda do
|
84
84
|
GamesDice::ComplexDie.new( 10, :maps => [ [7] ] )
|
85
|
-
end.should raise_error(
|
85
|
+
end.should raise_error( ArgumentError )
|
86
86
|
|
87
87
|
lambda do
|
88
88
|
GamesDice::ComplexDie.new( 10, :maps => ['hello'] )
|
data/spec/die_spec.rb
CHANGED
@@ -63,4 +63,20 @@ describe GamesDice::Die do
|
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
66
|
+
describe "#all_values" do
|
67
|
+
it "should return array with one result value per side" do
|
68
|
+
die = GamesDice::Die.new(8)
|
69
|
+
die.all_values.should == [1,2,3,4,5,6,7,8]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#each_value" do
|
74
|
+
it "should iterate through all sides of the die" do
|
75
|
+
die = GamesDice::Die.new(10)
|
76
|
+
arr = []
|
77
|
+
die.each_value { |x| arr << x }
|
78
|
+
arr.should == [1,2,3,4,5,6,7,8,9,10]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
66
82
|
end
|
data/spec/helpers.rb
CHANGED
@@ -1,4 +1,12 @@
|
|
1
1
|
# games_dice/spec/helpers.rb
|
2
|
+
require 'pathname'
|
3
|
+
require 'coveralls'
|
4
|
+
|
5
|
+
Coveralls.wear!
|
6
|
+
|
7
|
+
def fixture name
|
8
|
+
(Pathname.new(__FILE__).dirname + "fixtures" + name).to_s
|
9
|
+
end
|
2
10
|
|
3
11
|
# TestPRNG tests short predictable series
|
4
12
|
class TestPRNG
|
@@ -17,7 +25,7 @@ class TestPRNGMax
|
|
17
25
|
end
|
18
26
|
end
|
19
27
|
|
20
|
-
#
|
28
|
+
# TestPRNGMin checks behaviour of re-rolls
|
21
29
|
class TestPRNGMin
|
22
30
|
def rand(n)
|
23
31
|
1
|
data/spec/probability_spec.rb
CHANGED
@@ -516,4 +516,12 @@ describe GamesDice::Probabilities do
|
|
516
516
|
end # describe "#repeat_n_sum_k"
|
517
517
|
|
518
518
|
end # describe "instance methods"
|
519
|
+
|
520
|
+
describe "serialisation via Marshall" do
|
521
|
+
it "can load a saved GamesDice::Probabilities" do
|
522
|
+
pd6 = File.open( fixture('probs_fair_die_6.dat') ) { |file| Marshal.load(file) }
|
523
|
+
pd6.to_h.should be_valid_distribution
|
524
|
+
pd6.p_gt(4).should be_within(1e-10).of 1.0/3
|
525
|
+
end
|
526
|
+
end
|
519
527
|
end
|
metadata
CHANGED
@@ -1,134 +1,153 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: games_dice
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.8
|
5
5
|
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 3
|
9
|
-
- 7
|
10
|
-
version: 0.3.7
|
11
6
|
platform: ruby
|
12
|
-
authors:
|
7
|
+
authors:
|
13
8
|
- Neil Slater
|
14
9
|
autorequire:
|
15
10
|
bindir: bin
|
16
11
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2013-07-23 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
21
15
|
name: rspec
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
hash: 59
|
29
|
-
segments:
|
30
|
-
- 2
|
31
|
-
- 13
|
32
|
-
- 0
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
33
21
|
version: 2.13.0
|
34
22
|
type: :development
|
35
|
-
version_requirements: *id001
|
36
|
-
- !ruby/object:Gem::Dependency
|
37
|
-
name: rake
|
38
23
|
prerelease: false
|
39
|
-
|
40
|
-
none: false
|
41
|
-
requirements:
|
42
|
-
- -
|
43
|
-
- !ruby/object:Gem::Version
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.13.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
49
37
|
version: 1.9.1
|
50
38
|
type: :development
|
51
|
-
|
52
|
-
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.9.1
|
46
|
+
- !ruby/object:Gem::Dependency
|
53
47
|
name: yard
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.8.6
|
54
|
+
type: :development
|
54
55
|
prerelease: false
|
55
|
-
|
56
|
-
none: false
|
57
|
-
requirements:
|
58
|
-
- -
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
hash: 51
|
61
|
-
segments:
|
62
|
-
- 0
|
63
|
-
- 8
|
64
|
-
- 6
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
65
61
|
version: 0.8.6
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: coveralls
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.6.7
|
66
70
|
type: :development
|
67
|
-
version_requirements: *id003
|
68
|
-
- !ruby/object:Gem::Dependency
|
69
|
-
name: rake-compiler
|
70
71
|
prerelease: false
|
71
|
-
|
72
|
-
none: false
|
73
|
-
requirements:
|
74
|
-
- -
|
75
|
-
- !ruby/object:Gem::Version
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.6.7
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: json
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 1.7.7
|
80
86
|
type: :development
|
81
|
-
version_requirements: *id004
|
82
|
-
- !ruby/object:Gem::Dependency
|
83
|
-
name: redcarpet
|
84
87
|
prerelease: false
|
85
|
-
|
86
|
-
none: false
|
87
|
-
requirements:
|
88
|
-
- -
|
89
|
-
- !ruby/object:Gem::Version
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 1.7.7
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rake-compiler
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: redcarpet
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
95
117
|
version: 2.3.0
|
96
|
-
- - <
|
97
|
-
- !ruby/object:Gem::Version
|
98
|
-
hash: 7
|
99
|
-
segments:
|
100
|
-
- 3
|
101
|
-
- 0
|
102
|
-
- 0
|
103
|
-
version: 3.0.0
|
104
118
|
type: :development
|
105
|
-
version_requirements: *id005
|
106
|
-
- !ruby/object:Gem::Dependency
|
107
|
-
name: parslet
|
108
119
|
prerelease: false
|
109
|
-
|
110
|
-
none: false
|
111
|
-
requirements:
|
112
|
-
- -
|
113
|
-
- !ruby/object:Gem::Version
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 2.3.0
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: parslet
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
119
133
|
version: 1.5.0
|
120
134
|
type: :runtime
|
121
|
-
|
122
|
-
|
123
|
-
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: 1.5.0
|
142
|
+
description: A library for simulating dice. Use it to construct dice-rolling systems
|
143
|
+
used in role-playing and board games.
|
144
|
+
email:
|
124
145
|
- slobo777@gmail.com
|
125
146
|
executables: []
|
126
|
-
|
127
|
-
extensions:
|
147
|
+
extensions:
|
128
148
|
- ext/games_dice/extconf.rb
|
129
149
|
extra_rdoc_files: []
|
130
|
-
|
131
|
-
files:
|
150
|
+
files:
|
132
151
|
- .gitignore
|
133
152
|
- .travis.yml
|
134
153
|
- .yardopts
|
@@ -150,6 +169,7 @@ files:
|
|
150
169
|
- lib/games_dice/die.rb
|
151
170
|
- lib/games_dice/die_result.rb
|
152
171
|
- lib/games_dice/map_rule.rb
|
172
|
+
- lib/games_dice/marshal.rb
|
153
173
|
- lib/games_dice/parser.rb
|
154
174
|
- lib/games_dice/probabilities.rb
|
155
175
|
- lib/games_dice/reroll_rule.rb
|
@@ -159,6 +179,7 @@ files:
|
|
159
179
|
- spec/dice_spec.rb
|
160
180
|
- spec/die_result_spec.rb
|
161
181
|
- spec/die_spec.rb
|
182
|
+
- spec/fixtures/probs_fair_die_6.dat
|
162
183
|
- spec/helpers.rb
|
163
184
|
- spec/map_rule_spec.rb
|
164
185
|
- spec/parser_spec.rb
|
@@ -166,44 +187,44 @@ files:
|
|
166
187
|
- spec/readme_spec.rb
|
167
188
|
- spec/reroll_rule_spec.rb
|
168
189
|
homepage: https://github.com/neilslater/games_dice
|
169
|
-
licenses:
|
190
|
+
licenses:
|
170
191
|
- MIT
|
171
192
|
post_install_message:
|
172
193
|
rdoc_options: []
|
173
|
-
|
174
|
-
require_paths:
|
194
|
+
require_paths:
|
175
195
|
- lib
|
176
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
196
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
177
197
|
none: false
|
178
|
-
requirements:
|
179
|
-
- -
|
180
|
-
- !ruby/object:Gem::Version
|
181
|
-
|
182
|
-
segments:
|
198
|
+
requirements:
|
199
|
+
- - ! '>='
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
segments:
|
183
203
|
- 0
|
184
|
-
|
185
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
204
|
+
hash: 4486838003822106921
|
205
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
186
206
|
none: false
|
187
|
-
requirements:
|
188
|
-
- -
|
189
|
-
- !ruby/object:Gem::Version
|
190
|
-
|
191
|
-
segments:
|
207
|
+
requirements:
|
208
|
+
- - ! '>='
|
209
|
+
- !ruby/object:Gem::Version
|
210
|
+
version: '0'
|
211
|
+
segments:
|
192
212
|
- 0
|
193
|
-
|
213
|
+
hash: 4486838003822106921
|
194
214
|
requirements: []
|
195
|
-
|
196
215
|
rubyforge_project:
|
197
216
|
rubygems_version: 1.8.24
|
198
217
|
signing_key:
|
199
218
|
specification_version: 3
|
200
|
-
summary: Simulates and explains dice rolls from simple "1d6" to complex "roll 7 ten-sided
|
201
|
-
|
219
|
+
summary: Simulates and explains dice rolls from simple "1d6" to complex "roll 7 ten-sided
|
220
|
+
dice, take best 3, results of 10 roll again and add on".
|
221
|
+
test_files:
|
202
222
|
- spec/bunch_spec.rb
|
203
223
|
- spec/complex_die_spec.rb
|
204
224
|
- spec/dice_spec.rb
|
205
225
|
- spec/die_result_spec.rb
|
206
226
|
- spec/die_spec.rb
|
227
|
+
- spec/fixtures/probs_fair_die_6.dat
|
207
228
|
- spec/helpers.rb
|
208
229
|
- spec/map_rule_spec.rb
|
209
230
|
- spec/parser_spec.rb
|