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.
@@ -8,3 +8,7 @@ rvm:
8
8
  - ree
9
9
  - jruby-18mode
10
10
  - jruby-19mode
11
+ - jruby-head
12
+ allow_failures:
13
+ rvm:
14
+ - ruby-head
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # GamesDice
2
-
2
+ [![Gem Version](https://badge.fury.io/rb/games_dice.png)](http://badge.fury.io/rb/games_dice)
3
3
  [![Build Status](https://travis-ci.org/neilslater/games_dice.png?branch=master)](http://travis-ci.org/neilslater/games_dice)
4
+ [![Coverage Status](https://coveralls.io/repos/neilslater/games_dice/badge.png?branch=master)](https://coveralls.io/r/neilslater/games_dice?branch=master)
5
+ [![Code Climate](https://codeclimate.com/github/neilslater/games_dice.png)](https://codeclimate.com/github/neilslater/games_dice)
6
+ [![Dependency Status](https://gemnasium.com/neilslater/games_dice.png)](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 unsigned longs . . . the size of this structure sets the
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
- VALUE obj = pl_alloc( Probabilities );
750
- ProbabilityList *pl = get_probability_list( obj );
751
- double error;
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;
@@ -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.
@@ -13,6 +13,7 @@ require "games_dice/complex_die"
13
13
  require "games_dice/bunch"
14
14
  require "games_dice/dice"
15
15
  require "games_dice/parser"
16
+ require "games_dice/marshal"
16
17
 
17
18
  module GamesDice
18
19
  # @!visibility private
@@ -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
- @name = options[:name].to_s
36
- @ndice = Integer(options[:ndice])
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
- needs_complex_die = false
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
- raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{options[:keep_mode].inspect}"
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 #probabilites
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
- @min_result, @max_result = [probabilities.min, probabilities.max]
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
- @min_result, @max_result = [probabilities.min, probabilities.max]
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
- if @rerolls
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
- rule = @rerolls[ rule_idx ]
152
- rerolls_remaining[ rule_idx ] -= 1
153
- subtracting = true if rule.type == :reroll_subtract
139
+ def roll_apply_rerolls
140
+ return unless @rerolls
141
+ subtracting = false
142
+ rerolls_remaining = @rerolls.map { |rule| rule.limit }
154
143
 
155
- # Apply the rule (note reversal for additions, after a subtract)
156
- if subtracting && rule.type == :reroll_add
157
- @result.add_roll( @basic_die.roll, :reroll_subtract )
158
- else
159
- @result.add_roll( @basic_die.roll, rule.type )
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
- end
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
- # apply any mapping
165
- if @maps
166
- m, n = calc_maps(@result.value)
167
- @result.apply_map( m, n )
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
- @result
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
- private
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
- return nil unless rerolls_input
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
- return nil unless maps_input
190
- raise TypeError, "maps should be an Array, instead got #{maps_input.inspect}" unless maps_input.is_a?(Array)
191
- maps_input.map do |map_item|
192
- case map_item
193
- when Array then GamesDice::MapRule.new( map_item[0], map_item[1], map_item[2], map_item[3] )
194
- when GamesDice::MapRule then map_item
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 maps should be GamesDice::MapRule or Array, instead got #{map_item.inspect}"
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
- min_result = 1
221
- max_result = @basic_die.sides
222
- return [min_result,max_result] unless @rerolls || @maps
223
- return minmax_mappings( (min_result..max_result) ) unless @rerolls
224
- can_subtract = false
225
- @rerolls.each do |rule|
226
- next unless rule.type == :reroll_add || rule.type == :reroll_subtract
227
- min_reroll,max_reroll = (1..@basic_die.sides).select { |v| rule.applies?( v ) }.minmax
228
- next unless min_reroll && max_reroll
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
- can_subtract=true
231
- min_result = min_reroll - @basic_die.sides
242
+ min_subtract = min_reroll if min_subtract.nil?
243
+ min_subtract = min_reroll if min_subtract > min_reroll
232
244
  else
233
- max_result += max_reroll * rule.limit
245
+ total_add += max_reroll * rule.limit
234
246
  end
235
247
  end
236
- if can_subtract
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
- (1..@basic_die.sides).each do |v|
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
 
@@ -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 + @bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
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 + @bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
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 + @bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
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
- sum_parts = [ array.first < 0 ? '-' + array.first.abs.to_s : array.first.to_s ]
139
- sum_parts += array.drop(1).map { |n| n < 0 ? '- ' + n.abs.to_s : '+ ' + n.to_s }
140
- sum_parts += [ '=', array.inject(:+) ]
141
- sum_parts.join(' ')
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
@@ -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
- probs_a.each_with_index do |pa,i|
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
- new_probs = Array.new( 1 + combined_max - combined_min, 0.0 )
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
- revbin.each_with_index do |use_power, i|
255
- if use_power
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
- pd_power = GamesDice::Probabilities.add_distributions( pd_power, pd_power ) unless i == max_power
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
- if kmode == :keep_best
329
- keep_distributions = [ GamesDice::Probabilities.new( [1.0], q * k ) ]
330
- if p_gt(q) > 0.0 && k > 1
331
- kd_probabilities = given_ge( q + 1 )
332
- (1...k).each do |n|
333
- extra_o = GamesDice::Probabilities.new( [1.0], q * ( k - n ) )
334
- n_probs = kd_probabilities.repeat_sum( n )
335
- keep_distributions[n] = GamesDice::Probabilities.add_distributions( extra_o, n_probs )
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
- if kmode == :keep_best
353
+ case kmode
354
+ when :keep_best
356
355
  p_kept = p_gt(q)
357
356
  p_rejected = p_lt(q)
358
- elsif kmode == :keep_worst
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
@@ -1,3 +1,3 @@
1
1
  module GamesDice
2
- VERSION = "0.3.7"
2
+ VERSION = "0.3.8"
3
3
  end
@@ -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( TypeError )
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( TypeError )
85
+ end.should raise_error( ArgumentError )
86
86
 
87
87
  lambda do
88
88
  GamesDice::ComplexDie.new( 10, :maps => ['hello'] )
@@ -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
@@ -0,0 +1,2 @@
1
+ u:GamesDice::Probabilities�{ if0.16666666666666666if0.16666666666666666if0.16666666666666666i f0.16666666666666666i
2
+ f0.16666666666666666i f0.16666666666666666
@@ -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
- # TestPRNGMax checks behaviour of re-rolls
28
+ # TestPRNGMin checks behaviour of re-rolls
21
29
  class TestPRNGMin
22
30
  def rand(n)
23
31
  1
@@ -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
- hash: 29
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
- date: 2013-07-17 00:00:00 Z
19
- dependencies:
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
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
24
- none: false
25
- requirements:
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
- requirement: &id002 !ruby/object:Gem::Requirement
40
- none: false
41
- requirements:
42
- - - ">="
43
- - !ruby/object:Gem::Version
44
- hash: 49
45
- segments:
46
- - 1
47
- - 9
48
- - 1
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
- version_requirements: *id002
52
- - !ruby/object:Gem::Dependency
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
- requirement: &id003 !ruby/object:Gem::Requirement
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
- requirement: &id004 !ruby/object:Gem::Requirement
72
- none: false
73
- requirements:
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- hash: 3
77
- segments:
78
- - 0
79
- version: "0"
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
- requirement: &id005 !ruby/object:Gem::Requirement
86
- none: false
87
- requirements:
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- hash: 3
91
- segments:
92
- - 2
93
- - 3
94
- - 0
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
- requirement: &id006 !ruby/object:Gem::Requirement
110
- none: false
111
- requirements:
112
- - - ">="
113
- - !ruby/object:Gem::Version
114
- hash: 3
115
- segments:
116
- - 1
117
- - 5
118
- - 0
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
- version_requirements: *id006
122
- description: A library for simulating dice. Use it to construct dice-rolling systems used in role-playing and board games.
123
- email:
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
- hash: 3
182
- segments:
198
+ requirements:
199
+ - - ! '>='
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ segments:
183
203
  - 0
184
- version: "0"
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
- hash: 3
191
- segments:
207
+ requirements:
208
+ - - ! '>='
209
+ - !ruby/object:Gem::Version
210
+ version: '0'
211
+ segments:
192
212
  - 0
193
- version: "0"
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 dice, take best 3, results of 10 roll again and add on".
201
- test_files:
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