games_dice 0.3.7 → 0.3.8

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